diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f23c2af20..3731589e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,13 @@ repos: - repo: local hooks: + - id: license + name: Spotless + files: ".*\\.(java|kt|kts)$" + language: system + entry: ./gradlew spotlessApply + pass_filenames: false + require_serial: true - id: checkstyle name: Check Java codestyle files: ".*\\.java$" @@ -31,18 +38,11 @@ repos: entry: ./gradlew checkstyleMain checkstyleTest pass_filenames: false require_serial: true - - id: license - name: Check Java license headers - files: ".*\\.java$" - language: system - entry: ./gradlew licenseFormat - pass_filenames: false - require_serial: true - id: illuaminate name: Check Lua code files: ".*\\.(lua|java|md)" language: system - entry: ./gradlew lintLua -i + entry: ./gradlew lintLua pass_filenames: false require_serial: true diff --git a/build.gradle b/build.gradle index 6b248a47d..8559c9150 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ plugins { id "checkstyle" id "jacoco" id "maven-publish" - id "org.cadixdev.licenser" version "0.6.1" id "com.matthewprenger.cursegradle" version "1.4.0" id "com.github.breadmoirai.github-release" version "2.2.12" id "org.jetbrains.kotlin.jvm" version "1.7.0" @@ -11,7 +10,8 @@ plugins { id "org.spongepowered.mixin" version "0.7.+" id "org.parchmentmc.librarian.forgegradle" version "1.+" id "com.github.johnrengelman.shadow" version "7.1.2" - id "cc-tweaked.illuaminate" + id("cc-tweaked.illuaminate") + id("cc-tweaked.java-convention") } @@ -388,23 +388,6 @@ jacocoTestReport { test.finalizedBy("jacocoTestReport") -license { - header = file('config/license/main.txt') - lineEnding = '\n' - newLine = false - - properties { - year = Calendar.getInstance().get(Calendar.YEAR) - } - - include("**/*.java") // We could apply to Kotlin, but for now let's not - matching("dan200/computercraft/api/**") { - header = file('config/license/api.txt') - } -} - -check.dependsOn("licenseCheck") - def lintLua = tasks.register("lintLua", IlluaminateExec.class) { group = JavaBasePlugin.VERIFICATION_GROUP description = "Lint Lua (and Lua docs) with illuaminate" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index fc582ed5b..c71bab99a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,10 @@ repositories { gradlePluginPortal() } +dependencies { + implementation(libs.spotless) +} + gradlePlugin { plugins { register("cc-tweaked.illuaminate") { diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 000000000..b5a0fabf6 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts new file mode 100644 index 000000000..ca2ac0a90 --- /dev/null +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -0,0 +1,48 @@ +import cc.tweaked.gradle.LicenseHeader +import com.diffplug.gradle.spotless.FormatExtension +import com.diffplug.spotless.LineEnding +import java.nio.charset.StandardCharsets + +plugins { + java + jacoco + id("com.diffplug.spotless") +} + +spotless { + encoding = StandardCharsets.UTF_8 + lineEndings = LineEnding.UNIX + + fun FormatExtension.defaults() { + endWithNewline() + trimTrailingWhitespace() + indentWithSpaces(4) + } + + val licenser = LicenseHeader.create( + api = file("config/license/api.txt"), + main = file("config/license/main.txt"), + ) + + java { + defaults() + addStep(licenser) + removeUnusedImports() + } + + val ktlintConfig = mapOf( + "disabled_rules" to "no-wildcard-imports", + "ij_kotlin_allow_trailing_comma" to "true", + "ij_kotlin_allow_trailing_comma_on_call_site" to "true", + ) + + kotlinGradle { + defaults() + ktlint().editorConfigOverride(ktlintConfig) + } + + kotlin { + defaults() + ktlint().editorConfigOverride(ktlintConfig) + } +} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CheckLicense.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CheckLicense.kt new file mode 100644 index 000000000..2217d26be --- /dev/null +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CheckLicense.kt @@ -0,0 +1,73 @@ +package cc.tweaked.gradle + +import com.diffplug.spotless.FormatterFunc +import com.diffplug.spotless.FormatterStep +import com.diffplug.spotless.generic.LicenseHeaderStep +import java.io.File +import java.io.Serializable +import java.nio.charset.StandardCharsets + +/** + * Similar to [LicenseHeaderStep], but supports multiple licenses. + */ +object LicenseHeader { + /** + * The current year to use in templates. Intentionally not dynamic to avoid failing the build. + */ + private const val YEAR = 2022 + + private val COMMENT = Regex("""^/\*(.*?)\*/\n?""", RegexOption.DOT_MATCHES_ALL) + + fun create(api: File, main: File): FormatterStep = FormatterStep.createLazy( + "license", + { Licenses(getTemplateText(api), getTemplateText(main)) }, + { state -> FormatterFunc.NeedsFile { contents, file -> formatFile(state, contents, file) } }, + ) + + private fun getTemplateText(file: File): String = + file.readText(StandardCharsets.UTF_8).trim().replace("\${year}", "$YEAR") + + private fun formatFile(licenses: Licenses, contents: String, file: File): String { + val license = getLicense(contents) + val expectedLicense = getExpectedLicense(licenses, file.parentFile) + + return when { + license == null -> setLicense(expectedLicense, contents) + license.second != expectedLicense -> setLicense(expectedLicense, contents, license.first) + else -> contents + } + } + + private fun getExpectedLicense(licenses: Licenses, file: File): String { + var file: File? = file + while (file != null) { + if (file.name == "api" && file.parentFile?.name == "computercraft") return licenses.api + file = file.parentFile + } + return licenses.main + } + + private fun getLicense(contents: String): Pair? { + val match = COMMENT.find(contents) ?: return null + val license = match.groups[1]!!.value + .trim().lineSequence() + .map { it.trimStart(' ', '*') } + .joinToString("\n") + return Pair(match.range.last + 1, license) + } + + private fun setLicense(license: String, contents: String, start: Int = 0): String { + val out = StringBuilder() + out.append("/*\n") + for (line in license.lineSequence()) out.append(" * ").append(line).append("\n") + out.append(" */\n") + out.append(contents, start, contents.length) + return out.toString() + } + + private data class Licenses(val api: String, val main: String) : Serializable { + companion object { + private const val serialVersionUID: Long = 7741106448372435662L + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01d108214..57fe2e8f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,9 @@ hamcrest = "2.2" jqwik = "1.7.0" junit = "5.9.1" +# Build tools +spotless = "6.8.0" + [libraries] autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } jetbrainsAnnotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" } @@ -23,6 +26,9 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } +# Gradle plugins +spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } + [bundles] kotlin = ["kotlin-stdlib", "kotlin-coroutines"] diff --git a/settings.gradle.kts b/settings.gradle.kts index ede2e9c1e..153e5d000 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,4 +14,4 @@ pluginManagement { } val mc_version: String by settings -rootProject.name = "cc-tweaked-${mc_version}" +rootProject.name = "cc-tweaked-$mc_version" diff --git a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt index 450241a2b..1839a8efb 100644 --- a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt +++ b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt @@ -19,7 +19,6 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime - abstract class NullApiEnvironment : IAPIEnvironment { private val computerEnv = BasicEnvironment() diff --git a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt index 206924cc0..334af4b5e 100644 --- a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt +++ b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt @@ -28,8 +28,8 @@ class TestHttpApi { ComputerCraft.httpRules = Collections.unmodifiableList( listOf( AddressRule.parse("\$private", null, Action.DENY.toPartial()), - AddressRule.parse("*", null, Action.ALLOW.toPartial()) - ) + AddressRule.parse("*", null, Action.ALLOW.toPartial()), + ), ) } } diff --git a/src/testMod/java/dan200/computercraft/ingame/ComputerTest.kt b/src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt similarity index 93% rename from src/testMod/java/dan200/computercraft/ingame/ComputerTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt index 7935c518a..53c37d078 100644 --- a/src/testMod/java/dan200/computercraft/ingame/ComputerTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/Computer_Test.kt @@ -25,7 +25,8 @@ class Computer_Test { context.assertBlockState( lamp, { !it.getValue(RedstoneLampBlock.LIT) }, - { "Lamp should still not be lit" }) + { "Lamp should still not be lit" }, + ) } } } diff --git a/src/testMod/java/dan200/computercraft/ingame/CraftOsTest.kt b/src/testMod/java/dan200/computercraft/ingame/CraftOs_Test.kt similarity index 100% rename from src/testMod/java/dan200/computercraft/ingame/CraftOsTest.kt rename to src/testMod/java/dan200/computercraft/ingame/CraftOs_Test.kt diff --git a/src/testMod/java/dan200/computercraft/ingame/DiskDriveTest.kt b/src/testMod/java/dan200/computercraft/ingame/Disk_Drive_Test.kt similarity index 100% rename from src/testMod/java/dan200/computercraft/ingame/DiskDriveTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Disk_Drive_Test.kt diff --git a/src/testMod/java/dan200/computercraft/ingame/ModemTest.kt b/src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt similarity index 69% rename from src/testMod/java/dan200/computercraft/ingame/ModemTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt index 552151a09..2858a0cf5 100644 --- a/src/testMod/java/dan200/computercraft/ingame/ModemTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/Modem_Test.kt @@ -15,10 +15,14 @@ class Modem_Test { this .thenComputerOk(marker = "initial") .thenExecute { - helper.setBlock(position, BlockCable.correctConnections( - helper.level, helper.absolutePos(position), - Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true) - )) + helper.setBlock( + position, + BlockCable.correctConnections( + helper.level, + helper.absolutePos(position), + Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true), + ), + ) } .thenComputerOk() } diff --git a/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt b/src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt similarity index 99% rename from src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt index 4f291b53c..7fc6b5580 100644 --- a/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/Monitor_Test.kt @@ -24,7 +24,7 @@ class Monitor_Test { val toSet = BlockStateInput( Registry.ModBlocks.MONITOR_ADVANCED.get().defaultBlockState(), Collections.emptySet(), - tag + tag, ) context.setBlock(pos, Blocks.AIR.defaultBlockState()) diff --git a/src/testMod/java/dan200/computercraft/ingame/PrintoutTest.kt b/src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt similarity index 94% rename from src/testMod/java/dan200/computercraft/ingame/PrintoutTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt index 12b3ac7d7..0de4d44fe 100644 --- a/src/testMod/java/dan200/computercraft/ingame/PrintoutTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/Printout_Test.kt @@ -2,7 +2,7 @@ package dan200.computercraft.ingame import dan200.computercraft.ingame.api.* -class PrintoutTest { +class Printout_Test { @GameTest(batch = "client:Printout_Test.In_frame_at_night", timeoutTicks = Timeouts.CLIENT_TIMEOUT) fun In_frame_at_night(helper: GameTestHelper) = helper.sequence { this diff --git a/src/testMod/java/dan200/computercraft/ingame/TurtleTest.kt b/src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt similarity index 97% rename from src/testMod/java/dan200/computercraft/ingame/TurtleTest.kt rename to src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt index 6245d89ad..d6d6b876f 100644 --- a/src/testMod/java/dan200/computercraft/ingame/TurtleTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/Turtle_Test.kt @@ -86,9 +86,9 @@ class Turtle_Test { DetailRegistries.ITEM_STACK.addProvider( object : BasicItemDetailProvider("printout", ItemPrintout::class.java) { override fun provideDetails(data: MutableMap, stack: ItemStack, item: ItemPrintout) { - data["type"] = item.type.toString(); + data["type"] = item.type.toString() } - } + }, ) } .thenComputerOk() diff --git a/src/testMod/java/dan200/computercraft/ingame/api/GameTestHelper.kt b/src/testMod/java/dan200/computercraft/ingame/api/GameTestHelper.kt index 9bd69f120..b6f5fe841 100644 --- a/src/testMod/java/dan200/computercraft/ingame/api/GameTestHelper.kt +++ b/src/testMod/java/dan200/computercraft/ingame/api/GameTestHelper.kt @@ -79,7 +79,8 @@ typealias GameTestSequence = TestList typealias GameTestAssertException = RuntimeException -private fun GameTestSequence.addResult(task: () -> Unit, delay: Long? = null): GameTestSequence { +private fun GameTestSequence.addResult(task: () -> Unit) = addResult(null, task) +private fun GameTestSequence.addResult(delay: Long?, task: () -> Unit): GameTestSequence { val result = UnsafeHacks.newInstance(TestTickResult::class.java) as TestTickResult result.assertion = Runnable(task) result.expectedDelay = delay @@ -88,29 +89,29 @@ private fun GameTestSequence.addResult(task: () -> Unit, delay: Long? = null): G } fun GameTestSequence.thenSucceed() { - addResult({ + addResult { if (parent.error != null) return@addResult parent.finish() parent.markAsComplete() - }) + } } fun GameTestSequence.thenWaitUntil(task: () -> Unit) = addResult(task) -fun GameTestSequence.thenExecute(task: () -> Unit) = addResult({ +fun GameTestSequence.thenExecute(task: () -> Unit) = addResult { try { task() } catch (e: Exception) { parent.fail(e) } -}) +} -fun GameTestSequence.thenIdle(delay: Long) = addResult({ +fun GameTestSequence.thenIdle(delay: Long) = addResult { if (parent.tickCount < lastTick + delay) { throw GameTestAssertException("Waiting") } -}) +} /** * Proguard strips out all the "on success" code as it's never called anywhere. This is workable most of the time, but diff --git a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt b/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt index 9f34b99c1..dd5a72fce 100644 --- a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt +++ b/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt @@ -99,7 +99,7 @@ fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence { val screenshotPath = screenshotsPath.resolve("$fullName.png") val originalPath = TestMod.sourceDir.resolve("screenshots").resolve("$fullName.png") - if (!Files.exists(originalPath)) throw GameTestAssertException("$fullName does not exist. Use `/cctest promote' to create it."); + if (!Files.exists(originalPath)) throw GameTestAssertException("$fullName does not exist. Use `/cctest promote' to create it.") val screenshot = ImageIO.read(screenshotPath.toFile()) ?: throw GameTestAssertException("Error reading screenshot from $screenshotPath") @@ -147,14 +147,16 @@ fun GameTestHelper.positionAtArmorStand() { player.connection.teleport(stand.x, stand.y, stand.z, stand.yRot, stand.xRot) } - class ClientTestHelper { val minecraft: Minecraft = Minecraft.getInstance() fun screenshot(name: String, callback: () -> Unit = {}) { ScreenShotHelper.grab( - minecraft.gameDirectory, name, - minecraft.window.width, minecraft.window.height, minecraft.mainRenderTarget + minecraft.gameDirectory, + name, + minecraft.window.width, + minecraft.window.height, + minecraft.mainRenderTarget, ) { TestMod.log.info(it.string) callback()