diff --git a/.github/workflows/build_status.yml b/.github/workflows/build_status.yml index aa6ccda9..56758789 100644 --- a/.github/workflows/build_status.yml +++ b/.github/workflows/build_status.yml @@ -23,6 +23,9 @@ jobs: steps: - name: Check out source uses: actions/checkout@v7 + with: + # Spotless ratchets from origin/master-2.0-1.21.1-neo, so that branch must be present. + fetch-depth: 0 - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v6 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b95b36ab..8eb2ed8f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,9 @@ jobs: steps: - name: Check out source uses: actions/checkout@v7 + with: + # Spotless ratchets from origin/master-2.0-1.21.1-neo, so that branch must be present. + fetch-depth: 0 - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v6 diff --git a/build.gradle b/build.gradle index fba6a9fb..f9d810c4 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,12 @@ plugins { id 'idea' id 'maven-publish' id 'net.neoforged.moddev' version '2.0.148' + id 'com.diffplug.spotless' version '8.9.0' } +// Import-block step for Spotless and its fixture tests (docs/code-format.md). +apply from: 'gradle/import-blocks.gradle' + def env = System.getenv() def tag = providers.environmentVariable('TAG') version = tag.orElse("${mod_version}-development").get() @@ -211,33 +215,24 @@ tasks.named('javadoc', Javadoc) { options.links('https://docs.oracle.com/en/java/javase/21/docs/api/') } -// Formatting: `./gradlew format` applies Google Java Style with google-java-format, then splits -// imports into blank-line-separated groups (scripts/group_imports.py). The formatter jar is not -// committed: put google-java-format's all-deps jar at tools/gjf.jar (or pass -PgjfJar=). +// Formatting (docs/code-format.md): Eclipse JDT with the shared profile in +// gradle/java-formatter.xml (VS Code uses the same one), then the import-block step. +// Ratcheted: only files changed since the base branch are checked and formatted, so code is +// brought over to this style as it is next edited. Never reformat unrelated files in a change. +// spotlessCheck is part of check/build; `./gradlew spotlessApply` (or `format`) fixes files. +spotless { + ratchetFrom 'origin/master-2.0-1.21.1-neo' + java { + target 'src/**/*.java' + eclipse('4.35').configFile('gradle/java-formatter.xml') + custom 'importBlocks', importBlocksStep + } +} + tasks.register('format') { group = 'formatting' - description = 'Formats all Java sources (Google style, grouped imports).' - def jar = file(providers.gradleProperty('gjfJar').getOrElse('tools/gjf.jar')) - def python = providers.gradleProperty('mmdlibPython').getOrElse('python3') - def sources = fileTree('src') { include '**/*.java' } - def java = "${System.getProperty('java.home')}/bin/java" - doLast { - if (!jar.isFile()) - throw new GradleException("google-java-format not found at ${jar}; see build.gradle") - // Imports are left in place here and grouped by the script below instead. - // Keep each invocation below Windows' command-line length limit as the library grows. - sources.files.toList().collate(40).each { batch -> - providers.exec { - commandLine([java, '-jar', jar.absolutePath, '--skip-sorting-imports', '--replace'] + - batch.collect { it.absolutePath }) - }.result.get() - } - def grouping = providers.exec { - commandLine python, file('scripts/group_imports.py').absolutePath, file('src').absolutePath - } - grouping.result.get() - logger.lifecycle("Formatted ${sources.files.size()} files. " + grouping.standardOutput.asText.get().trim()) - } + description = 'Formats changed Java sources; an alias for spotlessApply.' + dependsOn 'spotlessApply' } idea { diff --git a/docs/code-format.md b/docs/code-format.md new file mode 100644 index 00000000..c91daa3c --- /dev/null +++ b/docs/code-format.md @@ -0,0 +1,77 @@ +# Code format + +Java follows the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) +with these overrides: + +- **Indentation: 4 spaces for both blocks and continuations.** Wrapped expressions and array + initializers add one four-space level, not eight spaces. Existing explicit line breaks are kept + where the formatter supports it. The 100-column limit and the other Google conventions (brace + style, one statement per line, naming, `@Override` always, no wildcard imports, ...) still apply. +- **Imports are split into blocks, one per second-level package, separated by a single blank + line.** Order: + 1. All static imports, as one block. + 2. `java.*` blocks, then `javax.*` blocks. + 3. Third-party blocks in ASCII order of their second-level prefix (`com.google`, `com.mojang`, + `mezz.jei`, `net.minecraft`, `net.neoforged`, `org.slf4j`, ...). + 4. MMDLib's own imports (`zone.moddev.*`, the library and the companion) last, as one block. + + Within a block, imports are ASCII-sorted. Example: + + ```java + import java.util.List; + import java.util.Map; + + import com.mojang.logging.LogUtils; + import com.mojang.serialization.Codec; + + import net.minecraft.core.BlockPos; + import net.minecraft.world.level.Level; + + import net.neoforged.bus.api.IEventBus; + + import org.slf4j.Logger; + + import zone.moddev.mc.companion.CompanionMod; + import zone.moddev.mc.lib.MMDLib; + ``` + +## Code written before this style + +Most of the code predates this style: it uses google-java-format's 2-space indentation. It is +**not** reformatted in bulk. A file is brought over to the new style when it is next edited, as +part of that change, and only then. Don't reformat files a change doesn't otherwise touch. + +## Tools + +[Spotless](https://github.com/diffplug/spotless) does the formatting: + +- `./gradlew spotlessApply` (or `./gradlew format`) formats files. +- `./gradlew spotlessCheck` reports files that aren't formatted. It is part of `check` and `build`, + so CI fails on them. + +Both are **ratcheted** from `origin/master-2.0-1.21.1-neo`: they only look at files that differ +from that branch, which is how older files are left alone until edited. Keep that branch fetched; +CI checks out with full history for this reason. + +The Java formatter is Eclipse JDT with the `MMDLib` profile in `gradle/java-formatter.xml`. Imports +are regrouped afterwards by a custom step, `gradle/import-blocks.gradle`, since Spotless's own +`importOrder` can't express rule 3. The step refuses, with the line number, input it can't rewrite +safely: comments between imports, more than one import or a trailing comment on a line, and +wildcard imports. Its fixtures in `gradle/import-blocks/` run as `testImportBlocks`, part of +`check`. + +### VS Code + +The same profile formats in VS Code. `.vscode/` is not committed, so add this to your +`.vscode/settings.json`: + +```json +"java.format.settings.url": "gradle/java-formatter.xml", +"java.format.settings.profile": "MMDLib", +"[java]": { + "editor.tabSize": 4, + "editor.insertSpaces": true +} +``` + +VS Code's formatter doesn't apply the import-block step; run `./gradlew format` before committing. diff --git a/gradle/import-blocks.gradle b/gradle/import-blocks.gradle new file mode 100644 index 00000000..c198ae0d --- /dev/null +++ b/gradle/import-blocks.gradle @@ -0,0 +1,226 @@ +/* + * Import-block formatting step for Spotless, enforcing the import layout in + * docs/code-format.md. The Eclipse JDT formatter runs first without reorganizing imports; + * this step then rewrites the import section into blank-line-separated blocks: + * + * 1. all static imports, as one block; + * 2. one block per second-level `java.*` package (`java.io`, `java.util`, ...), then the same + * for `javax.*`; + * 3. one block per third-party second-level package, in ASCII order of that prefix + * (`com.google`, `com.mojang`, `net.minecraft`, `net.neoforged`, `org.slf4j`, ...); + * 4. this project's own imports (`zone.moddev.*`: MMDLib and its companion) last, as one block. + * + * Imports inside a block are ASCII-sorted and exact duplicates are dropped. Spotless's built-in + * `importOrder` can't express rule 3 without a hand-maintained list of every prefix, which is why + * this is a custom step. + * + * Input the step can't rewrite safely is rejected with an error naming the line, never guessed at: + * comments between imports (they would lose their position), more than one import or a trailing + * comment on an import line, and wildcard imports (forbidden by Google style, which + * docs/code-format.md follows). + * + * Applied from build.gradle, this script exposes: + * - `importBlocksStep`: a closure for `spotless { java { custom 'importBlocks', ... } } }`; + * - `testImportBlocks`: a task running the fixtures in gradle/import-blocks/ through the step, + * wired into `check`. + */ + +/** + * The formatter. Stateless; all methods are static and thread-safe. + */ +class ImportBlocks { + + /** + * Package of this project's own code (MMDLib's `zone.moddev.mc.lib` and the companion's + * `zone.moddev.mc.companion`), always the last block, as one block. + */ + static final String OWN_PREFIX = 'zone.moddev' + + /** One import statement, as the step accepts it: a dotted name, optionally static. */ + private static final java.util.regex.Pattern IMPORT_LINE = ~/^import\s+(static\s+)?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(\.\*)?)\s*;\s*$/ + + /** + * Rewrites the import section of one Java source file. + * + * The section runs from the first line starting with `import ` to the last import line + * before the first line that is neither blank, an import, nor a comment. Comments after the + * last import (such as the class Javadoc) are outside the section and kept as they are. + * + * @param source the whole file, with `\n` line endings (Spotless normalizes them before any + * step runs) + * @return the file with its import section regrouped; unchanged if it has no imports + * @throws IllegalArgumentException if the import section holds anything the step can't + * rewrite safely; the message names the 1-based line number + */ + static String format(String source) { + List lines = source.split('\n', -1) as List + int first = lines.findIndexOf { it.startsWith('import ') } + if (first < 0) { + return source + } + + // Find where the section ends: the last import before the first line of real code. + int last = first + for (int i = first; i < lines.size(); i++) { + String line = lines[i].trim() + if (line.startsWith('import ')) { + last = i + } else if (!line.isEmpty() && !isComment(line)) { + break + } + } + + Set statics = new TreeSet<>() + Map> blocks = new TreeMap<>(BLOCK_ORDER) + for (int i = first; i <= last; i++) { + String line = lines[i] + if (line.trim().isEmpty()) { + continue + } + int lineNumber = i + 1 + if (isComment(line.trim())) { + throw new IllegalArgumentException("line ${lineNumber}: comments between imports " + + 'are not supported, since regrouping would move them away from the ' + + 'import they describe; move the comment out of the import section') + } + def match = IMPORT_LINE.matcher(line) + if (!match.matches()) { + throw new IllegalArgumentException("line ${lineNumber}: expected exactly one " + + "import statement with nothing else on the line, found: ${line.trim()}") + } + String name = match.group(2) + if (match.group(3) != null) { + throw new IllegalArgumentException("line ${lineNumber}: wildcard imports are not " + + "allowed (Google style, docs/code-format.md): ${line.trim()}") + } + if (match.group(1) != null) { + statics.add("import static ${name};".toString()) + } else { + blocks.computeIfAbsent(blockKey(name)) { new TreeSet() } + .add("import ${name};".toString()) + } + } + + List section = [] + List> groups = (statics.isEmpty() ? [] : [statics]) + blocks.values() + groups.eachWithIndex { Set group, int index -> + if (index > 0) { + section.add('') + } + section.addAll(group) + } + + List result = [] + result.addAll(lines.subList(0, first)) + result.addAll(section) + result.addAll(lines.subList(last + 1, lines.size())) + return result.join('\n') + } + + /** + * The block an import belongs to: {@link #OWN_PREFIX} for this project's own code, otherwise + * its first two name segments (`net.minecraft` for `net.minecraft.core.BlockPos`). A name with + * only two segments has a one-segment package (`lombok.Data`), so its block is that package + * alone. + */ + private static String blockKey(String name) { + if (name.startsWith(OWN_PREFIX + '.')) { + return OWN_PREFIX + } + String[] segments = name.split('\\.') + return segments.length >= 3 ? "${segments[0]}.${segments[1]}".toString() : segments[0] + } + + /** Rank of a block's category: `java`, then `javax`, then third-party, then this project. */ + private static int category(String key) { + String top = key.contains('.') ? key.substring(0, key.indexOf('.')) : key + if (top == 'java') { + return 0 + } + if (top == 'javax') { + return 1 + } + if (key == OWN_PREFIX) { + return 3 + } + return 2 + } + + /** Orders blocks by category, then by ASCII order of their key. */ + private static final Comparator BLOCK_ORDER = { String a, String b -> + int byCategory = Integer.compare(category(a), category(b)) + return byCategory != 0 ? byCategory : a <=> b + } as Comparator + + /** Whether a trimmed line is part of a comment (`//`, `/*`, `*`, or a closing `*\/`). */ + private static boolean isComment(String trimmed) { + return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') + } +} + +ext.importBlocksStep = { String source -> ImportBlocks.format(source) } + +/* + * Fixture tests for the step. Each case in gradle/import-blocks/ is `.input.txt` plus either + * `.expected.txt` (the exact output, which must also come back unchanged when formatted + * again) or `.error.txt` (text the step's error message must contain). + */ +def importBlocksFixtures = file('gradle/import-blocks') +def importBlocksStamp = layout.buildDirectory.file('import-blocks/tests-passed') + +tasks.register('testImportBlocks') { + group = 'verification' + description = 'Runs the import-block formatter fixtures in gradle/import-blocks/.' + inputs.dir(importBlocksFixtures) + inputs.file('gradle/import-blocks.gradle') + outputs.file(importBlocksStamp) + + doLast { + List cases = importBlocksFixtures.listFiles() + .findAll { it.name.endsWith('.input.txt') } + .sort { it.name } + if (cases.isEmpty()) { + throw new GradleException("No fixtures found in ${importBlocksFixtures}") + } + List failures = [] + cases.each { File input -> + String name = input.name - '.input.txt' + File expected = new File(importBlocksFixtures, "${name}.expected.txt") + File error = new File(importBlocksFixtures, "${name}.error.txt") + if (expected.exists() == error.exists()) { + failures.add("${name}: needs exactly one of .expected.txt or .error.txt") + return + } + String source = input.getText('UTF-8') + if (expected.exists()) { + String want = expected.getText('UTF-8') + String got = ImportBlocks.format(source) + if (got != want) { + failures.add("${name}: output differs\n--- expected\n${want}\n--- got\n${got}") + } else if (ImportBlocks.format(want) != want) { + failures.add("${name}: formatting the expected output again changed it") + } + } else { + String wantMessage = error.getText('UTF-8').trim() + try { + ImportBlocks.format(source) + failures.add("${name}: expected an error containing '${wantMessage}', got none") + } catch (IllegalArgumentException e) { + if (!e.message.contains(wantMessage)) { + failures.add("${name}: error '${e.message}' doesn't contain '${wantMessage}'") + } + } + } + } + if (!failures.isEmpty()) { + throw new GradleException("Import-block fixtures failed:\n\n" + failures.join('\n\n')) + } + File stamp = importBlocksStamp.get().asFile + stamp.parentFile.mkdirs() + stamp.text = "${cases.size()} fixtures passed\n" + } +} + +tasks.named('check') { + dependsOn 'testImportBlocks' +} diff --git a/gradle/import-blocks/already-formatted.expected.txt b/gradle/import-blocks/already-formatted.expected.txt new file mode 100644 index 00000000..1d4fbf0a --- /dev/null +++ b/gradle/import-blocks/already-formatted.expected.txt @@ -0,0 +1,30 @@ +package zone.moddev.mc.lib.dimension; + +import static java.util.Objects.requireNonNull; +import static zone.moddev.mc.lib.mana.Mana.RAW_PER_SET; + +import java.io.IOException; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; + +import org.slf4j.Logger; + +import zone.moddev.mc.lib.mana.Mana; +import zone.moddev.mc.lib.mana.ManaType; + +/** Class Javadoc stays where it is. */ +public final class Example {} diff --git a/gradle/import-blocks/already-formatted.input.txt b/gradle/import-blocks/already-formatted.input.txt new file mode 100644 index 00000000..1d4fbf0a --- /dev/null +++ b/gradle/import-blocks/already-formatted.input.txt @@ -0,0 +1,30 @@ +package zone.moddev.mc.lib.dimension; + +import static java.util.Objects.requireNonNull; +import static zone.moddev.mc.lib.mana.Mana.RAW_PER_SET; + +import java.io.IOException; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; + +import org.slf4j.Logger; + +import zone.moddev.mc.lib.mana.Mana; +import zone.moddev.mc.lib.mana.ManaType; + +/** Class Javadoc stays where it is. */ +public final class Example {} diff --git a/gradle/import-blocks/comment-between-imports.error.txt b/gradle/import-blocks/comment-between-imports.error.txt new file mode 100644 index 00000000..9d749c6d --- /dev/null +++ b/gradle/import-blocks/comment-between-imports.error.txt @@ -0,0 +1 @@ +line 4: comments between imports are not supported diff --git a/gradle/import-blocks/comment-between-imports.input.txt b/gradle/import-blocks/comment-between-imports.input.txt new file mode 100644 index 00000000..99f009d6 --- /dev/null +++ b/gradle/import-blocks/comment-between-imports.input.txt @@ -0,0 +1,7 @@ +package zone.moddev.mc.lib; + +import java.util.List; +// Needed for the codec. +import java.util.Map; + +public final class Example {} diff --git a/gradle/import-blocks/duplicates-and-blank-lines.expected.txt b/gradle/import-blocks/duplicates-and-blank-lines.expected.txt new file mode 100644 index 00000000..9c5fc9cc --- /dev/null +++ b/gradle/import-blocks/duplicates-and-blank-lines.expected.txt @@ -0,0 +1,6 @@ +package zone.moddev.mc.lib; + +import java.util.List; +import java.util.Map; + +public final class Example {} diff --git a/gradle/import-blocks/duplicates-and-blank-lines.input.txt b/gradle/import-blocks/duplicates-and-blank-lines.input.txt new file mode 100644 index 00000000..905bc328 --- /dev/null +++ b/gradle/import-blocks/duplicates-and-blank-lines.input.txt @@ -0,0 +1,9 @@ +package zone.moddev.mc.lib; + +import java.util.List; + + +import java.util.List; +import java.util.Map; + +public final class Example {} diff --git a/gradle/import-blocks/full-grouping.expected.txt b/gradle/import-blocks/full-grouping.expected.txt new file mode 100644 index 00000000..1d4fbf0a --- /dev/null +++ b/gradle/import-blocks/full-grouping.expected.txt @@ -0,0 +1,30 @@ +package zone.moddev.mc.lib.dimension; + +import static java.util.Objects.requireNonNull; +import static zone.moddev.mc.lib.mana.Mana.RAW_PER_SET; + +import java.io.IOException; + +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.ImmutableList; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; + +import org.slf4j.Logger; + +import zone.moddev.mc.lib.mana.Mana; +import zone.moddev.mc.lib.mana.ManaType; + +/** Class Javadoc stays where it is. */ +public final class Example {} diff --git a/gradle/import-blocks/full-grouping.input.txt b/gradle/import-blocks/full-grouping.input.txt new file mode 100644 index 00000000..97bf5d64 --- /dev/null +++ b/gradle/import-blocks/full-grouping.input.txt @@ -0,0 +1,21 @@ +package zone.moddev.mc.lib.dimension; + +import org.slf4j.Logger; +import zone.moddev.mc.lib.mana.Mana; +import java.util.Map; +import net.neoforged.fml.common.Mod; +import static java.util.Objects.requireNonNull; +import javax.annotation.Nullable; +import com.mojang.serialization.Codec; +import java.io.IOException; +import net.minecraft.core.BlockPos; +import java.util.List; +import com.google.common.collect.ImmutableList; +import com.mojang.logging.LogUtils; +import static zone.moddev.mc.lib.mana.Mana.RAW_PER_SET; +import net.minecraft.world.level.Level; +import net.neoforged.bus.api.IEventBus; +import zone.moddev.mc.lib.mana.ManaType; + +/** Class Javadoc stays where it is. */ +public final class Example {} diff --git a/gradle/import-blocks/lib-and-companion-share-a-block.expected.txt b/gradle/import-blocks/lib-and-companion-share-a-block.expected.txt new file mode 100644 index 00000000..c5b6809e --- /dev/null +++ b/gradle/import-blocks/lib-and-companion-share-a-block.expected.txt @@ -0,0 +1,11 @@ +package zone.moddev.mc.companion; + +import mezz.jei.api.IModPlugin; + +import org.slf4j.Logger; + +import zone.moddev.mc.companion.CompanionMod; +import zone.moddev.mc.lib.MMDLib; +import zone.moddev.mc.lib.api.BlockContext; + +public final class Example {} diff --git a/gradle/import-blocks/lib-and-companion-share-a-block.input.txt b/gradle/import-blocks/lib-and-companion-share-a-block.input.txt new file mode 100644 index 00000000..c345dc60 --- /dev/null +++ b/gradle/import-blocks/lib-and-companion-share-a-block.input.txt @@ -0,0 +1,9 @@ +package zone.moddev.mc.companion; + +import zone.moddev.mc.lib.MMDLib; +import org.slf4j.Logger; +import zone.moddev.mc.companion.CompanionMod; +import zone.moddev.mc.lib.api.BlockContext; +import mezz.jei.api.IModPlugin; + +public final class Example {} diff --git a/gradle/import-blocks/no-imports.expected.txt b/gradle/import-blocks/no-imports.expected.txt new file mode 100644 index 00000000..9606ac47 --- /dev/null +++ b/gradle/import-blocks/no-imports.expected.txt @@ -0,0 +1,4 @@ +package zone.moddev.mc.lib; + +/** No imports at all. */ +public final class Example {} diff --git a/gradle/import-blocks/no-imports.input.txt b/gradle/import-blocks/no-imports.input.txt new file mode 100644 index 00000000..9606ac47 --- /dev/null +++ b/gradle/import-blocks/no-imports.input.txt @@ -0,0 +1,4 @@ +package zone.moddev.mc.lib; + +/** No imports at all. */ +public final class Example {} diff --git a/gradle/import-blocks/one-segment-package.expected.txt b/gradle/import-blocks/one-segment-package.expected.txt new file mode 100644 index 00000000..f27b45a1 --- /dev/null +++ b/gradle/import-blocks/one-segment-package.expected.txt @@ -0,0 +1,8 @@ +package zone.moddev.mc.lib; + +import lombok.Data; +import lombok.Value; + +import org.slf4j.Logger; + +public final class Example {} diff --git a/gradle/import-blocks/one-segment-package.input.txt b/gradle/import-blocks/one-segment-package.input.txt new file mode 100644 index 00000000..94dcb7b8 --- /dev/null +++ b/gradle/import-blocks/one-segment-package.input.txt @@ -0,0 +1,7 @@ +package zone.moddev.mc.lib; + +import org.slf4j.Logger; +import lombok.Data; +import lombok.Value; + +public final class Example {} diff --git a/gradle/import-blocks/static-wildcard.error.txt b/gradle/import-blocks/static-wildcard.error.txt new file mode 100644 index 00000000..0de2a0c7 --- /dev/null +++ b/gradle/import-blocks/static-wildcard.error.txt @@ -0,0 +1 @@ +line 3: wildcard imports are not allowed diff --git a/gradle/import-blocks/static-wildcard.input.txt b/gradle/import-blocks/static-wildcard.input.txt new file mode 100644 index 00000000..d2861515 --- /dev/null +++ b/gradle/import-blocks/static-wildcard.input.txt @@ -0,0 +1,5 @@ +package zone.moddev.mc.lib; + +import static java.util.Objects.*; + +public final class Example {} diff --git a/gradle/import-blocks/trailing-comment.error.txt b/gradle/import-blocks/trailing-comment.error.txt new file mode 100644 index 00000000..51d3c7a5 --- /dev/null +++ b/gradle/import-blocks/trailing-comment.error.txt @@ -0,0 +1 @@ +line 3: expected exactly one import statement diff --git a/gradle/import-blocks/trailing-comment.input.txt b/gradle/import-blocks/trailing-comment.input.txt new file mode 100644 index 00000000..ee1da392 --- /dev/null +++ b/gradle/import-blocks/trailing-comment.input.txt @@ -0,0 +1,5 @@ +package zone.moddev.mc.lib; + +import java.util.List; // for levels + +public final class Example {} diff --git a/gradle/import-blocks/two-on-one-line.error.txt b/gradle/import-blocks/two-on-one-line.error.txt new file mode 100644 index 00000000..51d3c7a5 --- /dev/null +++ b/gradle/import-blocks/two-on-one-line.error.txt @@ -0,0 +1 @@ +line 3: expected exactly one import statement diff --git a/gradle/import-blocks/two-on-one-line.input.txt b/gradle/import-blocks/two-on-one-line.input.txt new file mode 100644 index 00000000..a4b56b3c --- /dev/null +++ b/gradle/import-blocks/two-on-one-line.input.txt @@ -0,0 +1,5 @@ +package zone.moddev.mc.lib; + +import java.util.List; import java.util.Map; + +public final class Example {} diff --git a/gradle/import-blocks/wildcard.error.txt b/gradle/import-blocks/wildcard.error.txt new file mode 100644 index 00000000..0de2a0c7 --- /dev/null +++ b/gradle/import-blocks/wildcard.error.txt @@ -0,0 +1 @@ +line 3: wildcard imports are not allowed diff --git a/gradle/import-blocks/wildcard.input.txt b/gradle/import-blocks/wildcard.input.txt new file mode 100644 index 00000000..3cccfbea --- /dev/null +++ b/gradle/import-blocks/wildcard.input.txt @@ -0,0 +1,5 @@ +package zone.moddev.mc.lib; + +import java.util.*; + +public final class Example {} diff --git a/gradle/java-formatter.xml b/gradle/java-formatter.xml new file mode 100644 index 00000000..49fe2bdc --- /dev/null +++ b/gradle/java-formatter.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/group_imports.py b/scripts/group_imports.py deleted file mode 100644 index 4cd13eea..00000000 --- a/scripts/group_imports.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -"""Splits Java imports into blank-line-separated groups, keeping the sorted order within each group. - -Run after google-java-format (which sorts imports into one block), or run google-java-format with ---skip-sorting-imports afterwards. Usage: scripts/group_imports.py [files or directories...] -(default: src). -""" -import re -import sys -from pathlib import Path - -# Static imports first (as Google style), then these prefixes in order; anything else goes in its own -# group (by top-level package) after "org.", and the project's own packages come last. -GROUPS = ["java.", "javax.", "com.google.", "com.mojang.", "net.minecraft.", "net.neoforged.", "org."] -OWN = "zone.moddev." -IMPORT = re.compile(r"^import (static )?([\w.]+(?:\.\*)?);$") - - -def group_key(line): - static, name = IMPORT.match(line).groups() - if static: return (0, "", name) - if name.startswith(OWN): return (3, "", name) - for index, prefix in enumerate(GROUPS): - if name.startswith(prefix): return (1, f"{index:02d}", name) - return (2, name.split(".")[0], name) - - -def regroup(text): - lines = text.split("\n") - indices = [i for i, line in enumerate(lines) if IMPORT.match(line)] - if not indices: return text - first, last = indices[0], indices[-1] - # Only blank lines may sit between imports; anything else means an unusual file, left alone. - if any(lines[i].strip() and not IMPORT.match(lines[i]) for i in range(first, last + 1)): return text - imports = sorted({lines[i] for i in indices}, key=group_key) - block, previous = [], None - for line in imports: - key = group_key(line)[:2] - if previous is not None and key != previous: block.append("") - block.append(line); previous = key - return "\n".join(lines[:first] + block + lines[last + 1:]) - - -def main(): - targets = [Path(arg) for arg in sys.argv[1:]] or [Path("src")] - files = [f for t in targets for f in (sorted(t.rglob("*.java")) if t.is_dir() else [t])] - changed = 0 - for file in files: - text = file.read_text() - updated = regroup(text) - if updated != text: file.write_text(updated); changed += 1 - print(f"Regrouped imports in {changed} of {len(files)} files") - - -if __name__ == "__main__": - main()