diff --git a/.editorconfig b/.editorconfig index 9fe2fbc6c6..a9d4bb29e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,19 +1,19 @@ root = true [*] -indent_style = tab -indent_size = 4 +indent_style = space +indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 # md (GitHub shows code fences with 8 spaces) [*.md] -indent_style = space -indent_size = 2 trim_trailing_whitespace = false [*.java] +indent_style = tab +indent_size = 4 # Doc: https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 ij_java_imports_layout = $*,|,java.**,|,javax.**,|,org.**,|,com.**,|,com.diffplug.**,|,* ij_java_use_single_class_imports = true @@ -21,11 +21,11 @@ ij_java_class_count_to_use_import_on_demand = 999 ij_java_names_count_to_use_import_on_demand = 999 [*.xml.mustache] -indent_style = space +indent_size = 4 -[*.{yml,yaml}] -indent_style = space -indent_size = 2 +[*.template] +indent_style = tab +indent_size = 4 # Prevent unexpected automatic indentation when crafting test-cases [/testlib/src/main/resources/**] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..00483c1e0b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Changes to these paths can move secrets, publish artifacts, or change what ships to users, +# so they need an owner's review. Takes effect once branch protection on main requires +# approvals and "require review from Code Owners". +/.github/ @nedtwigg @edgartwigg +/build-logic/ @nedtwigg @edgartwigg +/settings.gradle.kts @nedtwigg @edgartwigg +/build.gradle.kts @nedtwigg @edgartwigg +/gradle.properties @nedtwigg @edgartwigg diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 210c929ecb..a6601f9214 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,20 +1,35 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended", - ], - packageRules: [ - { - groupName: 'Ktlint', - matchPackageNames: [ - 'com.pinterest.ktlint:*', - ] - }, - { - groupName: 'OpenRewrite', - matchPackageNames: [ - 'org.openrewrite.recipe:*', - ], - }, - ] + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ], + packageRules: [ + { + enabled: false, + matchFileNames: [ + 'gradle/pinned-libs.versions.toml', + ], + }, + { + groupName: 'Ktlint', + matchPackageNames: [ + 'com.pinterest.ktlint:*', + ], + }, + { + groupName: 'OpenRewrite', + matchPackageNames: [ + 'org.openrewrite.recipe:*', + ], + }, + { + // 7.40.0+ requires artifacts from the authenticated Code Genome Project repository. + // Keep builds accessible without a token until we opt into that migration. + // https://github.com/openrewrite/rewrite-gradle-plugin/issues/476 + matchPackageNames: [ + 'org.openrewrite.rewrite:org.openrewrite.rewrite.gradle.plugin', + ], + allowedVersions: '<7.40.0', + }, + ], } diff --git a/.github/scripts/publish-release.sh b/.github/scripts/publish-release.sh new file mode 100644 index 0000000000..14e6ce3400 --- /dev/null +++ b/.github/scripts/publish-release.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +case "${1:-}" in + lib) + changelog_prefix=: + publish_tasks=(:lib:publishToMavenCentral :lib-extra:publishToMavenCentral) + ;; + plugin-gradle) + changelog_prefix=:plugin-gradle: + publish_tasks=(:plugin-gradle:publishPlugins :plugin-gradle:publishToMavenCentral) + ;; + plugin-maven) + changelog_prefix=:plugin-maven: + publish_tasks=(:plugin-maven:publishToMavenCentral) + ;; + *) + echo 'Usage: publish-release.sh {lib|plugin-gradle|plugin-maven}' >&2 + exit 2 + ;; +esac + +gradle_args=(-Prelease=true -PmavenCentralDeploymentValidation=PUBLISHED + --stacktrace --warning-mode all --no-configuration-cache --no-scan) +if [[ "$1" == plugin-gradle ]]; then + gradle_args+=("-Pgradle.publish.key=${GRADLE_KEY:?}" "-Pgradle.publish.secret=${GRADLE_SECRET:?}") +fi + +# Central uploads happen at the end of a successful Gradle build. Finish that +# build, including waiting for PUBLISHED, before mutating the changelog or Git. +./gradlew "${changelog_prefix}changelogInternalPushWillRun" "${changelog_prefix}changelogCheck" \ + "${publish_tasks[@]}" "${gradle_args[@]}" + +# The artifacts are already published. Exclude the publishing dependencies from +# the changelog build so it cannot upload them again or defer publication to its end. +exclude_tasks=() +for task in "${publish_tasks[@]}"; do + exclude_tasks+=(-x "$task") +done +./gradlew "${changelog_prefix}changelogPush" -PpublicationConfirmed=true \ + "${exclude_tasks[@]}" "${gradle_args[@]}" diff --git a/.github/scripts/test_publish_release.py b/.github/scripts/test_publish_release.py new file mode 100644 index 0000000000..320d153295 --- /dev/null +++ b/.github/scripts/test_publish_release.py @@ -0,0 +1,70 @@ +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("publish-release.sh").resolve() +PUBLICATIONS = { + "lib": [":lib:publishToMavenCentral", ":lib-extra:publishToMavenCentral"], + "plugin-gradle": [":plugin-gradle:publishPlugins", ":plugin-gradle:publishToMavenCentral"], + "plugin-maven": [":plugin-maven:publishToMavenCentral"], +} + + +class ReleasePublicationTest(unittest.TestCase): + def run_release(self, component, publish_exit): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + wrapper = root / "gradlew" + wrapper.write_text( + "#!/usr/bin/env python3\n" + "import json, os, sys\n" + "with open('calls.jsonl', 'a') as calls:\n" + " calls.write(json.dumps(sys.argv[1:]) + '\\n')\n" + "if any(arg.endswith(':changelogPush') for arg in sys.argv):\n" + " open('tag-created', 'w').close()\n" + "else:\n" + " # Model a failure at build shutdown, after publication tasks finish.\n" + " sys.exit(int(os.environ['PUBLISH_EXIT']))\n" + ) + wrapper.chmod(0o755) + result = subprocess.run( + ["bash", str(SCRIPT), component], + cwd=root, + env=dict(os.environ, PUBLISH_EXIT=str(publish_exit), + GRADLE_KEY="test-key", GRADLE_SECRET="test-secret"), + capture_output=True, + text=True, + ) + calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()] + return result, calls, (root / "tag-created").exists() + + def test_failed_publication_never_updates_changelog_or_pushes_tag(self): + for component in PUBLICATIONS: + with self.subTest(component=component): + result, calls, tagged = self.run_release(component, 42) + self.assertEqual(result.returncode, 42) + self.assertEqual(len(calls), 1) + self.assertFalse(tagged) + self.assertNotIn("-PpublicationConfirmed=true", calls[0]) + + def test_confirmed_publication_precedes_changelog_without_republishing(self): + for component, publications in PUBLICATIONS.items(): + with self.subTest(component=component): + result, calls, tagged = self.run_release(component, 0) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(len(calls), 2) + self.assertTrue(tagged) + self.assertIn("-PmavenCentralDeploymentValidation=PUBLISHED", calls[0]) + self.assertNotIn("-PpublicationConfirmed=true", calls[0]) + self.assertIn("-PpublicationConfirmed=true", calls[1]) + for task in publications: + self.assertIn(task, calls[0]) + self.assertEqual(calls[1][calls[1].index(task) - 1], "-x") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/update_renovate_changelogs.py b/.github/scripts/update_renovate_changelogs.py new file mode 100755 index 0000000000..21698f0307 --- /dev/null +++ b/.github/scripts/update_renovate_changelogs.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +import argparse +import re +import subprocess +import sys +import tomllib +from pathlib import Path + +KEY_MAPPING = { + ("versions", "jackson"): "jackson", + ("versions", "ktlint"): "ktlint", + ("versions", "javaparser"): "javaparser", + ("libraries", "palantir-java-format"): "palantir-java-format", + ("libraries", "adocfmt"): "adocfmt", + ("libraries", "antlr4-formatter"): "antlr4-formatter", + ("libraries", "cleanthat-java"): "cleanthat", + ("libraries", "cool-rdf-formatter"): "cool-rdf-formatter", + ("libraries", "diktat-runner"): "diktat", + ("libraries", "flexmark-all"): "flexmark", + ("libraries", "freshmark"): "freshmark", + ("libraries", "gherkin-utils"): "gherkin-utils", + ("libraries", "google-java-format"): "google-java-format", + ("libraries", "gson"): "gson", + ("libraries", "json-simple"): "json-simple", + ("libraries", "ktfmt"): "ktfmt", + ("libraries", "prince-of-space-core"): "prince-of-space", + ("libraries", "scalafmt-core"): "scalafmt", + ("libraries", "sortpom-sorter"): "sortpom", + ("libraries", "tabletest-formatter-core"): "tabletest-formatter", + ("libraries", "zjsonpatch"): "zjsonpatch", +} + +CHANGELOG_FILES = [ + Path("CHANGES.md"), + Path("plugin-gradle/CHANGES.md"), + Path("plugin-maven/CHANGES.md"), +] + + +def extract_formatter_versions(toml_str: str) -> dict[str, str]: + data = tomllib.loads(toml_str) + versions = data.get("versions", {}) + libraries = data.get("libraries", {}) + result = {} + for (sec, key), tool_name in KEY_MAPPING.items(): + if sec == "versions": + if key in versions: + result[tool_name] = str(versions[key]) + elif sec == "libraries": + if key in libraries: + val = libraries[key] + if isinstance(val, str): + parts = val.split(":") + if len(parts) >= 3: + result[tool_name] = parts[-1] + elif isinstance(val, dict): + if "version" in val: + result[tool_name] = str(val["version"]) + elif "version.ref" in val and val["version.ref"] in versions: + result[tool_name] = str(versions[val["version.ref"]]) + return result + + +def insert_changelog_entries(content: str, entries: list[str]) -> str: + unreleased_match = re.search(r"(##\s*\[Unreleased\]\s*\n+)", content) + if not unreleased_match: + return content + + unreleased_start = unreleased_match.end() + next_release_match = re.search(r"\n##\s*\[", content[unreleased_start:]) + if next_release_match: + unreleased_end = unreleased_start + next_release_match.start() + unreleased_block = content[unreleased_start:unreleased_end] + after_block = content[unreleased_end:] + else: + unreleased_block = content[unreleased_start:] + after_block = "" + + new_entries = [e for e in entries if e not in unreleased_block] + if not new_entries: + return content + + entries_text = "\n".join(new_entries) + + changes_match = re.search(r"(###\s*Changes\s*\n)", unreleased_block) + if changes_match: + changes_header_end = changes_match.end() + # Find the end of ### Changes section (either next ### or next ## or end of block) + next_section_match = re.search(r"\n(###\s+\w+|##\s*\[)", unreleased_block[changes_header_end:]) + if next_section_match: + changes_end = changes_header_end + next_section_match.start() + changes_content = unreleased_block[changes_header_end:changes_end].rstrip() + rest = unreleased_block[changes_end:].lstrip("\n") + unreleased_block = ( + unreleased_block[:changes_header_end] + + (changes_content + "\n" if changes_content else "") + + entries_text + + "\n\n" + + rest + ) + else: + changes_content = unreleased_block[changes_header_end:].rstrip() + unreleased_block = ( + unreleased_block[:changes_header_end] + + (changes_content + "\n" if changes_content else "") + + entries_text + + "\n\n" + ) + else: + if unreleased_block.strip(): + unreleased_block = "### Changes\n" + entries_text + "\n\n" + unreleased_block.lstrip() + else: + unreleased_block = "### Changes\n" + entries_text + "\n\n" + + return content[:unreleased_start] + unreleased_block + after_block + + +def main() -> None: + parser = argparse.ArgumentParser(description="Update changelog files for Renovate version bumps.") + parser.add_argument("--base-ref", default="origin/main", help="Base ref or branch name to compare against") + parser.add_argument("--pr-number", required=True, help="Pull Request number") + parser.add_argument("--pr-url", required=True, help="Pull Request URL") + args = parser.parse_args() + + toml_path = Path("gradle/libs.versions.toml") + if not toml_path.exists(): + print(f"Error: {toml_path} does not exist.") + sys.exit(1) + + head_content = toml_path.read_text(encoding="utf-8") + + # Find the merge base commit between base_ref and HEAD so that only changes in this PR are considered + merge_base_proc = subprocess.run( + ["git", "merge-base", args.base_ref, "HEAD"], + capture_output=True, + text=True, + ) + if merge_base_proc.returncode == 0 and merge_base_proc.stdout.strip(): + compare_ref = merge_base_proc.stdout.strip() + else: + compare_ref = args.base_ref + + git_show_cmd = ["git", "show", f"{compare_ref}:gradle/libs.versions.toml"] + proc = subprocess.run(git_show_cmd, capture_output=True, text=True) + if proc.returncode != 0: + print(f"Warning: Failed to fetch base toml from {compare_ref}: {proc.stderr}") + sys.exit(0) + + base_content = proc.stdout + + base_versions = extract_formatter_versions(base_content) + head_versions = extract_formatter_versions(head_content) + + entries = [] + for tool_name, new_ver in head_versions.items(): + old_ver = base_versions.get(tool_name) + if old_ver and old_ver != new_ver: + entry = f"- Bump default `{tool_name}` version `{old_ver}` -> `{new_ver}`. ([#{args.pr_number}]({args.pr_url}))" + entries.append(entry) + + if not entries: + print("No formatter default versions changed in this branch.") + return + + print(f"Detected {len(entries)} formatter version update(s):") + for entry in entries: + print(f" {entry}") + + for changelog_file in CHANGELOG_FILES: + if not changelog_file.exists(): + continue + original = changelog_file.read_text(encoding="utf-8") + updated = insert_changelog_entries(original, entries) + if updated != original: + changelog_file.write_text(updated, encoding="utf-8") + print(f"Updated {changelog_file}") + else: + print(f"No changes needed for {changelog_file}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/changelog-print.yml b/.github/workflows/changelog-print.yml index e60fb3674b..e55cc0037c 100644 --- a/.github/workflows/changelog-print.yml +++ b/.github/workflows/changelog-print.yml @@ -4,13 +4,16 @@ on: push: branches: [main] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest name: changelogPrint steps: - - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 with: java-version: 21 distribution: 'temurin' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e544cbe197..26d2b0026d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,92 +1,173 @@ # BUILDCACHE_USER # BUILDCACHE_PASS # - rw access to buildcache.diffplug.com -name: CI ๐Ÿญ +name: CI on: pull_request: push: - branches: [main, release] + branches: [ main, release ] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + +# least privilege by default; jobs opt in to more below +permissions: + contents: read + jobs: sanity-check: - name: Sanity Check ๐Ÿฆข + name: Sanity Check runs-on: ubuntu-latest env: - buildcacheuser: ${{ secrets.BUILDCACHE_USER }} - buildcachepass: ${{ secrets.BUILDCACHE_PASS }} + # Only push to the remote build cache on push events to prevent cache pollution and poisoning from PRs. + buildcacheuser: ${{ github.event_name == 'push' && secrets.BUILDCACHE_USER || '' }} + buildcachepass: ${{ github.event_name == 'push' && secrets.BUILDCACHE_PASS || '' }} steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: "temurin" java-version: 21 - - name: gradle caching - uses: gradle/actions/setup-gradle@v6 - - name: Spotless โœจ - run: ./gradlew spotlessCheck - - name: assemble testClasses - run: ./gradlew assemble testClasses - build: + - uses: gradle/actions/setup-gradle@v6 + - name: Check release publication ordering + run: python3 .github/scripts/test_publish_release.py + - run: ./gradlew spotlessCheck + - run: ./gradlew assemble testClasses publishToMavenLocal + + build-gradle: + name: Gradle (Java ${{ matrix.jre }}, ${{ matrix.os }}) + needs: sanity-check + permissions: + contents: read + checks: write # mikepenz/action-junit-report posts check runs + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + jre: [ 17, 21, 25, 26 ] # Test on all supported LTS and latest JDK. + include: + - os: windows-latest + jre: 26 # Test on the latest JDK for speeds. + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: "temurin" + java-version: ${{ matrix.jre }} + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew build -x spotlessCheck -PSPOTLESS_EXCLUDE_MAVEN=true + - name: junit result + uses: mikepenz/action-junit-report@v6 + if: always() + with: + check_name: JUnit Gradle ${{ matrix.jre }} ${{ matrix.os }} + report_paths: '*/build/test-results/*/TEST-*.xml' + check_retries: true + + build-maven: + name: Maven (Java ${{ matrix.jre }}, ${{ matrix.os }}) needs: sanity-check + permissions: + contents: read + checks: write # mikepenz/action-junit-report posts check runs strategy: fail-fast: false matrix: - kind: [maven, gradle] - # Test on the latest Java version once Gradle & Maven support it. - jre: [17, 21, 24] - os: [ubuntu-latest, windows-latest] + os: [ ubuntu-latest ] + jre: [ 17, 21, 25, 26 ] # Test on all supported LTS and latest JDK. include: - # npm on linux only (crazy slow on windows) - - kind: npm - jre: 17 - os: ubuntu-latest - - kind: shfmt - jre: 17 - os: ubuntu-latest - shfmt-version: v3.8.0 - - kind: idea - jre: 17 - os: ubuntu-latest + - os: windows-latest + jre: 26 # Test on the latest JDK for speeds. runs-on: ${{ matrix.os }} steps: - - name: Checkout - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 with: distribution: "temurin" java-version: ${{ matrix.jre }} - - name: gradle caching - uses: gradle/actions/setup-gradle@v6 - - name: build (maven-only) - if: matrix.kind == 'maven' - run: ./gradlew :plugin-maven:build -x spotlessCheck - - name: build (everything-but-maven) - if: matrix.kind == 'gradle' - run: ./gradlew build -x spotlessCheck -PSPOTLESS_EXCLUDE_MAVEN=true - - name: test npm - if: matrix.kind == 'npm' - run: ./gradlew testNpm - - name: Setup go - if: matrix.kind == 'shfmt' - uses: actions/setup-go@v6 + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew :plugin-maven:build -x spotlessCheck + - name: junit result + uses: mikepenz/action-junit-report@v6 + if: always() + with: + check_name: JUnit Maven ${{ matrix.jre }} ${{ matrix.os }} + report_paths: '*/build/test-results/*/TEST-*.xml' + check_retries: true + + test-npm: + name: NPM + needs: sanity-check + permissions: + contents: read + checks: write # mikepenz/action-junit-report posts check runs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: "temurin" + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew testNpm + - name: junit result + uses: mikepenz/action-junit-report@v6 + if: always() + with: + check_name: JUnit NPM + report_paths: '*/build/test-results/*/TEST-*.xml' + check_retries: true + + test-shfmt: + name: Shfmt + needs: sanity-check + permissions: + contents: read + checks: write # mikepenz/action-junit-report posts check runs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: "temurin" + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - uses: actions/setup-go@v7 with: go-version: 'stable' - name: Install shfmt - if: matrix.kind == 'shfmt' run: | - go install mvdan.cc/sh/v3/cmd/shfmt@${{ matrix.shfmt-version }} + go install mvdan.cc/sh/v3/cmd/shfmt@v3.8.0 - name: Test shfmt - if: matrix.kind == 'shfmt' run: ./gradlew testShfmt + - name: junit result + uses: mikepenz/action-junit-report@v6 + if: always() + with: + check_name: JUnit shfmt + report_paths: '*/build/test-results/*/TEST-*.xml' + check_retries: true + + test-idea: + name: IntelliJ IDEA + needs: sanity-check + permissions: + contents: read + checks: write # mikepenz/action-junit-report posts check runs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: "temurin" + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 - name: Test idea - if: matrix.kind == 'idea' run: | download_link=$(curl https://data.services.jetbrains.com/products/releases\?code\=IIC\&latest\=true\&type\=release | jq -r '.IIC[0].downloads.linux.link') curl --location "$download_link" -o idea.tar.gz @@ -97,9 +178,9 @@ jobs: ./gradlew testIdea - name: junit result uses: mikepenz/action-junit-report@v6 - if: always() # always run even if the previous step fails + if: always() with: - check_name: JUnit ${{ matrix.kind }} ${{ matrix.jre }} ${{ matrix.os }} + check_name: JUnit IDEA report_paths: '*/build/test-results/*/TEST-*.xml' check_retries: true @@ -107,7 +188,11 @@ jobs: final-status: needs: - sanity-check - - build + - build-gradle + - build-maven + - test-npm + - test-shfmt + - test-idea runs-on: ubuntu-slim if: always() steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9f8f931c62..a468fcc8de 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 592704b781..a44ac86557 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,13 +1,18 @@ # NEXUS_USER -# NEXUS_PASS64 (base64 NOTE: `base64` and `openssl base64` failed, had to use Java -# byte[] data = "{{password}}".getBytes(StandardCharsets.UTF_8); -# String encoded = new String(Base64.getEncoder().encode(data), StandardCharsets.UTF_8); -# System.out.println(encoded); +# NEXUS_PASS # GPG_PASSPHRASE -# GPG_KEY64 (base64) -# gpg --export-secret-keys --armor KEY_ID | openssl base64 | pbcopy +# GPG_KEY +# gpg --export-secret-subkeys --armor SUBKEY_ID! | pbcopy +# subkeys only, so the primary key never lands in CI # GRADLE_KEY # GRADLE_SECRET +# +# NEXUS_PASS and GPG_KEY are environment secrets on `maven-central`, not org secrets. +# +# Store each secret in the exact form the build consumes. GitHub masks the value it was given, +# so anything a workflow step decodes or reshapes is no longer masked and lands in the log. +# That is why the base64 variants (NEXUS_PASS64, GPG_KEY64) were deleted on 2026-09-17; do not +# reintroduce them. name: deploy on: @@ -23,27 +28,49 @@ on: - plugin-maven - all - lib + - lib-recover + lib_version: + description: 'Existing lib tag version to publish when using lib-recover (e.g. 4.10.3)' + required: false + type: string jobs: build: runs-on: ubuntu-latest name: deploy + # holds the publishing secrets; its branch policy allows only `release`, which only + # nedtwigg can push to, so nothing else in the repo can reach them + environment: maven-central permissions: contents: write env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ORG_GRADLE_PROJECT_nexus_user: ${{ secrets.NEXUS_USER }} - ORG_GRADLE_PROJECT_nexus_pass64: ${{ secrets.NEXUS_PASS64 }} - ORG_GRADLE_PROJECT_gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - ORG_GRADLE_PROJECT_gpg_key64: ${{ secrets.GPG_KEY64 }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.NEXUS_USER }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.NEXUS_PASS }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + # signing subkey issued 2026-09-17, replacing 0x4272C851 whose encrypted copy was exposed + # in public CI logs: https://github.com/diffplug/spotless/issues/3089 + # Gradle requires the last 8 hex digits of subkey FA5C18CFB74EC7C6. + ORG_GRADLE_PROJECT_signingInMemoryKeyId: "0xB74EC7C6" + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSPHRASE }} steps: - - uses: actions/checkout@v6 + - name: Validate library recovery input + if: "${{ inputs.to_publish == 'lib-recover' }}" + env: + LIB_VERSION: ${{ inputs.lib_version }} + run: | + [[ "$LIB_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo 'lib-recover requires a released library version, e.g. 4.10.3' + exit 1 + } + - uses: actions/checkout@v7 with: persist-credentials: false + ref: ${{ inputs.to_publish == 'lib-recover' && format('refs/tags/lib/{0}', inputs.lib_version) || github.ref }} - name: Configure git credentials for JGit run: | git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${{ secrets.GITHUB_TOKEN }} | base64 -w0)" - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: java-version: 21 distribution: 'temurin' @@ -55,19 +82,50 @@ jobs: run: git -c http.version=HTTP/1.1 fetch origin main - name: publish all if: "${{ github.event.inputs.to_publish == 'all' }}" + env: + GRADLE_KEY: ${{ secrets.GRADLE_KEY }} + GRADLE_SECRET: ${{ secrets.GRADLE_SECRET }} run: | - ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache - ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} --stacktrace --warning-mode all --no-configuration-cache - ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh lib + bash .github/scripts/publish-release.sh plugin-gradle + bash .github/scripts/publish-release.sh plugin-maven - name: publish just plugin-gradle if: "${{ github.event.inputs.to_publish == 'plugin-gradle' }}" + env: + GRADLE_KEY: ${{ secrets.GRADLE_KEY }} + GRADLE_SECRET: ${{ secrets.GRADLE_SECRET }} run: | - ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh plugin-gradle - name: publish just plugin-maven if: "${{ github.event.inputs.to_publish == 'plugin-maven' }}" run: | - ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh plugin-maven - name: publish just lib if: "${{ github.event.inputs.to_publish == 'lib' }}" run: | - ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh lib + - name: Recover library artifacts from an existing tag + if: "${{ inputs.to_publish == 'lib-recover' }}" + env: + LIB_VERSION: ${{ inputs.lib_version }} + run: | + # Verify the build's versions without printing Gradle properties or secrets. + cat > "$RUNNER_TEMP/verify-library-version.gradle" <<'EOF' + gradle.projectsEvaluated { + if (rootProject.name != 'spotless') { + return // Init scripts also run for the included build-logic build. + } + [':lib', ':lib-extra'].each { projectPath -> + def library = rootProject.project(projectPath) + if (library.version.toString() != System.getenv('LIB_VERSION')) { + throw new GradleException("Library version does not match the recovery tag: ${projectPath}") + } + } + } + EOF + # End this build successfully so the publishing plugin uploads its bundle. + # Do not run changelogPush: the version and tag already exist. + ./gradlew :lib:publishToMavenCentral :lib-extra:publishToMavenCentral \ + -Prelease=true -PmavenCentralDeploymentValidation=PUBLISHED \ + --init-script "$RUNNER_TEMP/verify-library-version.gradle" \ + --stacktrace --warning-mode all --no-configuration-cache --no-scan diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml new file mode 100644 index 0000000000..eb383112e4 --- /dev/null +++ b/.github/workflows/release-snapshot.yml @@ -0,0 +1,53 @@ +# Publishes a snapshot to Maven Central, by hand. +# +# Snapshots are never published automatically -- a push to any branch cannot publish anything. +# Run this to exercise the publishing credentials and the signing key without cutting a release, +# which is worth doing after any change to publishing, signing, or the secrets themselves. +# +# It runs in the same environment as a release, so the environment's branch policy decides where +# it can be dispatched from. +name: release-snapshot + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + release-snapshot: + name: Publish snapshot + runs-on: ubuntu-latest + environment: maven-central + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/setup-java@v6 + with: + distribution: "temurin" + java-version: 21 + - uses: gradle/actions/setup-gradle@v6 + with: + cache-read-only: true + # a release is immutable on Central, so make sure this can only ever ship a snapshot + - name: Verify every published version is a snapshot + run: | + for project in :lib :lib-extra :plugin-gradle :plugin-maven; do + version=$(./gradlew -q --no-configuration-cache "$project:properties" | awk -F': ' '/^version:/{print $2}') + echo "$project: $version" + case "$version" in + *-SNAPSHOT) ;; + *) echo "refusing to publish non-snapshot version $version for $project"; exit 1 ;; + esac + done + # each secret is stored in the exact form the build consumes, so GitHub masks it in the logs + - run: ./gradlew publishToMavenCentral --no-configuration-cache + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.NEXUS_USER }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.NEXUS_PASS }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + # signing subkey issued 2026-09-17, replacing 0x4272C851 whose encrypted copy was exposed + # in public CI logs: https://github.com/diffplug/spotless/issues/3089 + ORG_GRADLE_PROJECT_signingInMemoryKeyId: "0xFA5C18CFB74EC7C6" + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/renovate-changelog.yml b/.github/workflows/renovate-changelog.yml new file mode 100644 index 0000000000..be4f44f42f --- /dev/null +++ b/.github/workflows/renovate-changelog.yml @@ -0,0 +1,48 @@ +name: Renovate Changelog Updater + +on: + pull_request: + types: [ opened, synchronize, reopened ] + paths: + - 'gradle/libs.versions.toml' + +jobs: + update-changelog: + name: Update Changelog + if: github.actor == 'renovate[bot]' || startsWith(github.head_ref, 'renovate/') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - uses: actions/setup-python@v7 + # branch and PR fields arrive through env, never spliced into the shell: a branch name is + # attacker-controlled text, and ${{ }} would run it as part of the command + - name: Update Changelogs + env: + BASE_REF: ${{ github.base_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + python3 .github/scripts/update_renovate_changelogs.py \ + --base-ref "origin/$BASE_REF" \ + --pr-number "$PR_NUMBER" \ + --pr-url "$PR_URL" + - name: Commit and push changes + env: + HEAD_REF: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ -n "$(git status --porcelain CHANGES.md plugin-gradle/CHANGES.md plugin-maven/CHANGES.md)" ]; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CHANGES.md plugin-gradle/CHANGES.md plugin-maven/CHANGES.md + git commit -m "Update changelogs for default formatter version bump (#$PR_NUMBER)" + git push origin "HEAD:$HEAD_REF" + else + echo "No changelog updates needed." + fi diff --git a/.github/workflows/scheduled-cleanup.yml b/.github/workflows/scheduled-cleanup.yml index 54b3cb6c55..8d4089309b 100644 --- a/.github/workflows/scheduled-cleanup.yml +++ b/.github/workflows/scheduled-cleanup.yml @@ -2,9 +2,13 @@ name: Scheduled Cleanup ๐Ÿงน on: schedule: - cron: '0 16 * * 3' + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: sanity: name: Sanity ๐Ÿฆข @@ -14,10 +18,10 @@ jobs: buildcachepass: ${{ secrets.BUILDCACHE_PASS }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-java@v5 + - uses: actions/setup-java@v6 with: distribution: temurin java-version: 21 @@ -27,3 +31,8 @@ jobs: run: ./gradlew assemble -Derror-prone=true - name: OpenRewrite โ˜‘๏ธ # 4m 55s (2 min compile, 3 min rewrite) run: ./gradlew rewriteDryRun + # Catches upstream drift between the embedded lockfiles and Eclipse's P2 metadata. It lives + # here rather than in CI because it queries the Eclipse update sites -- per-PR gaps and strays + # are already caught offline by EclipseJdtFormatterStepTest. + - name: Eclipse JDT Lockfiles ๐Ÿ”’ # ~1 min + run: ./gradlew :lib-extra:verifyEclipseJdtLockfiles diff --git a/CHANGES.md b/CHANGES.md index 5b0e8c042a..0c14924f58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,80 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +## [4.10.4] - 2026-10-08 + +### Changes +- Bump default Eclipse JDT formatter version from `4.40` to `4.41`. +- GitPrePushHookInstaller takes optional parameter for pre push hook directory, instead of always writing to .git/hooks. ([#3115](https://github.com/diffplug/spotless/pull/3115) +- Bump default `gherkin` version `42.0.0` -> `42.0.1`. ([#3088](https://github.com/diffplug/spotless/pull/3088)) +- Bump default `jackson` version `2.22.2` -> `2.22.3`. ([#3101](https://github.com/diffplug/spotless/pull/3101)) +- Bump default `palantir-java-format` version `2.98.0` -> `2.102.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103), [#3132](https://github.com/diffplug/spotless/pull/3132)) +- Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) +- Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. +- Bump default `palantir-java-format` version `2.99.0` -> `2.102.0`. ([#3132](https://github.com/diffplug/spotless/pull/3132)) + +### Fixed +- `shortenFullyQualifiedTypes` no longer corrupts a method reference on a field/variable (`localVar.field::method`), which JavaParser parses as a type-shaped scope; such scopes now go through the same known-package / minimum-depth heuristic as other expression scopes before being treated as fully-qualified types. ([#3133](https://github.com/diffplug/spotless/issues/3133)) +- `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) + +## [4.10.3] - 2026-09-25 + +### Changes +- Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `gson` version `2.13.2` -> `2.14.0`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `zjsonpatch` version `0.4.14` -> `0.4.16`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `jackson-dataformat-yaml` version `2.14.1` -> `2.20.1`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `ktfmt` version `0.63` -> `0.64`. ([2988](https://github.com/diffplug/spotless/pull/2988)) +- Bump default `cleanthat` version `2.25` -> `2.26`. ([#2882](https://github.com/diffplug/spotless/pull/2882)) +- Bump default `jackson` version `2.20.1` -> `2.22.2`. ([#2819](https://github.com/diffplug/spotless/pull/2819)) +- Bump default `javaparser` version `3.27.1` -> `3.28.2`. ([#3065](https://github.com/diffplug/spotless/pull/3065)) +- Bump default `palantir-java-format` version `2.80.0` -> `2.98.0`. ([#3068](https://github.com/diffplug/spotless/pull/3068)) +- Bump default `scalafmt` version `3.8.1` -> `3.11.5`. ([#2173](https://github.com/diffplug/spotless/pull/2173)) +- Bump default `google-java-format` version `1.30.0` -> `1.36.1`. ([#3075](https://github.com/diffplug/spotless/pull/3075)) +- Bump default `gherkin-utils` version `10.0.0` -> `12.0.2`. ([#2979](https://github.com/diffplug/spotless/pull/2979)) + +### Fixed +- Fix release signing by using Gradle's required eight-digit signing subkey ID. ([#3105](https://github.com/diffplug/spotless/pull/3105)) +- Fix race when creating the npm install cache directory. (([#3096](https://github.com/diffplug/spotless/pull/3096)) +- GrEclipse no longer emits expected OSGi and nested-jar warnings during initialization. ([#2445](https://github.com/diffplug/spotless/issues/2445)) +- `typescript` `prettier()` no longer emits a warning when its parser is already set to `typescript`. ([#3098](https://github.com/diffplug/spotless/pull/3098)) +- `VersionCatalogStep` preserves standalone comments at section boundaries and the end of the file. ([#3048](https://github.com/diffplug/spotless/issues/3048)) +- `VersionCatalogStep` preserves entries when comments contain unmatched brackets, preserves commas inside quoted strings, and keeps significant line boundaries in multiline entries. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- `VersionCatalogStep` now reports unfinished entries as lints at their starting line. These fail formatting by default, so upgrading may expose catalog errors that previously caused silent data loss. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- `FeatureClassLoader` now provides stub implementations of `lombok.*` classes (and synthesises empty classes for any unknown ones) so that the Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` or `NoSuchMethodError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [4.10.2] - 2026-09-04 +### Fixed +- `ShortenFullyQualifiedTypesStep` now shortens fully-qualified types used in expression contexts (such as static method calls, static fields, and enum constants) while avoiding imports that would change how existing unqualified type references resolve. ([#3039](https://github.com/diffplug/spotless/pull/3039)) +- `FeatureClassLoader` now provides stub implementations of `lombok.*` classes (and synthesises empty classes for any others) so that the Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [4.10.1] - 2026-08-27 +### Fixed +- Prettier and other npm-based formatters no longer fail to start on npm 12 (`EUNKNOWNCONFIG` from `--scripts-prepend-node-path`). ([#3024](https://github.com/diffplug/spotless/issues/3024)) + +## [4.10.0] - 2026-08-17 +### Added +- New `ShortenFullyQualifiedTypesStep` for Java, which replaces fully-qualified type names with their simple names and adds the imports they need. Uses JavaParser to find type references in the AST, so occurrences in strings, comments, and other non-type contexts are left alone. ([#2945](https://github.com/diffplug/spotless/issues/2945)) +- Add embedded lockfiles to Eclipse JDT for every supported version (`4.9` through `4.40`), so `eclipse()` resolves from Maven Central instead of querying a P2 update site. Versions without an embedded lockfile still fall back to P2 provisioning. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +### Fixed +- `removeUnusedImports` no longer throws on Java `import module` declarations (`JCModuleImport` ClassCastException). ([#2890](https://github.com/diffplug/spotless/issues/2890)) +- Concurrent P2 provisioning (parallel multi-project Gradle fingerprinting of `eclipse()` / `greclipse()` steps) no longer races Solstice's on-disk cache; also `ConfigurationCacheHackList.toString()` no longer evaluates step state (which could re-trigger provisioning while Gradle reports "cannot be serialized"). ([#3004](https://github.com/diffplug/spotless/issues/3004)) +### Changes +- Default `google-java-format` remains `1.28.0` on JVM 17; bumps to `1.30.0` on JVM 21+; require at least `1.30.0` on JVM 25+ for `import module` support. +- Bump default `eclipse` version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +- Bump default `adocfmt` version `0.2.0` -> `0.3.1`, which adds table formatting support (`formatTables`, `tableLayout`, `tableMaxLineWidth`, `tableBlankLines`). + +## [4.9.0] - 2026-07-27 +### Added +- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space). ([#2991](https://github.com/diffplug/spotless/pull/2991)) +### Changes +- Replace RDF formatter library `de.atextor:turtle-formatter` (discontinued) with `cool.rdf:cool-rdf-formatter` (its new coordinates); the RDF/turtle formatter now requires Java 25+. ([#2995](https://github.com/diffplug/spotless/pull/2995)) +- Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- Bump default `tabletest-formatter` version `1.1.1` -> `1.1.2`. + ## [4.8.0] - 2026-06-29 ### Added - Add support for custom string format for license header copyright year via `yearStringFormat()`. ([#2965](https://github.com/diffplug/spotless/pull/2965)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec94bba693..3fb6589e94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -196,7 +196,7 @@ the pipeline fails, first check if the code is formatted and no bugs were found. First, run `./gradlew publishToMavenLocal` in your local checkout of Spotless. Now, in any other project on your machine, you can use the following snippet in your `settings.gradle` (for Gradle 6.0+). -``` +```gradle pluginManagement { repositories { mavenLocal { @@ -206,47 +206,19 @@ pluginManagement { } gradlePluginPortal() } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'com.diffplug.spotless') { - useModule('com.diffplug.spotless:spotless-plugin-gradle:{latest-SNAPSHOT}') - } - } - } } ``` -### Gradle - any commit in a public GitHub repo (this one, or any fork) - -In Gradle 6.0+, you can use the following snippet in your `settings.gradle`. - - -```gradle -pluginManagement { - repositories { - maven { - url 'https://jitpack.io' - content { - includeGroup 'com.github.{{user-or-org}}.spotless' - } - } - gradlePluginPortal() - } - resolutionStrategy { - eachPlugin { - if (requested.id.id == 'com.diffplug.spotless') { - useModule('com.github.{{USER_OR_ORG}}.spotless:spotless-plugin-gradle:{{SHA_OF_COMMIT_YOU_WANT}}') - } - } - } -} -``` +### Gradle - snapshot builds -If it doesn't work, you can check the JitPack log at `https://jitpack.io/com/github/{{USER_OR_ORG}}/spotless/{{SHA_OF_COMMIT_YOU_WANT}}/build.log`. +Nothing is published automatically -- a push to any branch cannot publish anything. A maintainer can +publish a snapshot by hand with the `release-snapshot` workflow, which we use to exercise publishing +and signing before a release, so don't count on any particular snapshot existing. To test an +unreleased change, use the local workflow above. ### Maven -Run `./gradlew publishToMavenLocal` to publish this to your local repository. You can also use the JitPack artifacts, using the same principles as Gradle above. +Run `./gradlew publishToMavenLocal` to publish this to your local repository. ## License diff --git a/README.md b/README.md index 81f7891891..53fc88856f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | h You probably want one of the links below: -## [โ‡๏ธ Spotless for Gradle](plugin-gradle) (with integrations for [VS Code](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) and [IntelliJ](https://plugins.jetbrains.com/plugin/18321-spotless-gradle)) +## [โ‡๏ธ Spotless for Gradle](plugin-gradle) (with integrations for [VS Code](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) and IntelliJ with [Spotless Daemon](https://plugins.jetbrains.com/plugin/29287-spotless-daemon) or [Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle)) ```console user@machine repo % ./gradlew build @@ -84,10 +84,12 @@ extra('groovy.GrEclipseFormatterStep') +'{{yes}} | {{yes}} lib('java.GoogleJavaFormatStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.ImportOrderStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', +lib('java.PrinceOfSpaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.ExpandWildcardImportsStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('java.ForbidWildcardImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.ForbidModuleImportsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', +lib('java.ShortenFullyQualifiedTypesStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.FormatAnnotationsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.CleanthatJavaStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', @@ -143,10 +145,12 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | [`java.GoogleJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.ImportOrderStep`](lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`java.PrinceOfSpaceStep`](lib/src/main/java/com/diffplug/spotless/java/PrinceOfSpaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.ExpandWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`java.ForbidWildcardImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidWildcardImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.ForbidModuleImportsStep`](lib/src/main/java/com/diffplug/spotless/java/ForbidModuleImportsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`java.ShortenFullyQualifiedTypesStep`](lib/src/main/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.FormatAnnotationsStep`](lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.CleanthatJavaStep`](lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000000..3d1f29ca75 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + `kotlin-dsl` +} + +val versionCatalog = versionCatalogs.named("libs") + +fun plugin(alias: String): String { + val p = versionCatalog.findPlugin(alias).get().get() + return "${p.pluginId}:${p.pluginId}.gradle.plugin:${p.version}" +} + +dependencies { + implementation(plugin("spotless")) + implementation(plugin("spotless-changelog")) + implementation("com.diffplug.spotless-changelog:spotless-changelog-lib:3.1.2") + implementation(plugin("spotbugs")) + implementation(plugin("errorprone")) + implementation(plugin("rewrite")) + implementation(plugin("test-logger")) + implementation(plugin("maven-publish")) + implementation(plugin("develocity")) + + // TODO: https://github.com/gradle/gradle/issues/15383 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000000..a51240b49f --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,13 @@ +dependencyResolutionManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/Ext.kt b/build-logic/src/main/kotlin/Ext.kt new file mode 100644 index 0000000000..746d8f550a --- /dev/null +++ b/build-logic/src/main/kotlin/Ext.kt @@ -0,0 +1,23 @@ +import com.diffplug.spotless.changelog.gradle.ChangelogExtension +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.getByType + +internal val Project.libs: LibrariesForLibs + get() = extensions.findByType() ?: rootProject.extensions.getByType() + +val Project.spotlessChangelog: ChangelogExtension + get() = extensions.findByType() ?: rootProject.extensions.getByType() + +val Project.rootSpotlessChangelog: ChangelogExtension + get() = rootProject.extensions.getByType() + +fun Project.requiredProperty(name: String): String = + findProperty(name)?.toString() + ?: error("$path is missing the '$name' property, which is required") + +fun String.toBooleanGroovy(): Boolean = trim().lowercase() in setOf("true", "y", "1") + +val isCiServer: Boolean + get() = System.getenv().containsKey("CI") diff --git a/build-logic/src/main/kotlin/spotless.changelog.gradle.kts b/build-logic/src/main/kotlin/spotless.changelog.gradle.kts new file mode 100644 index 0000000000..6004ed1627 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.changelog.gradle.kts @@ -0,0 +1,63 @@ +plugins { + id("com.diffplug.spotless-changelog") +} + +val (kind, releaseTitle) = + when (project.name) { + "plugin-gradle" -> "gradle" to "Gradle Plugin" + "plugin-maven" -> "maven" to "Maven Plugin" + else -> { + check(project == rootProject) + "lib" to "Lib" + } + } + +// the root project and plugins have their own changelogs +spotlessChangelog { + changelogFile("CHANGES.md") + // need -Prelease=true in order to do a publish + setAppendDashSnapshotUnless_dashPrelease(true) + branch("release") + tagPrefix("$kind/") + commitMessage("Published $kind/{{version}}") // {{version}} will be replaced + tagMessage("{{changes}}") + runAfterPush( + "gh release create $kind/{{version}} --title '$releaseTitle v{{version}}' --notes-from-tag" + ) +} + +if (project == rootProject) { + gradle.taskGraph.whenReady { + val changelogPushTasks = allTasks.filter { it.name == "changelogPush" }.map { it.path } + if (changelogPushTasks.size > 1) { + // make sure only one changelog gets published per tag/commit + throw IllegalArgumentException( + "Run changelogPush one at a time:\n" + changelogPushTasks.joinToString("\n") + ) + } + if (changelogPushTasks.size == 1) { + check(rootProject.findProperty("publicationConfirmed")?.toString() == "true") { + "Publish and confirm the artifacts before pushing release tags. " + + "Use bash .github/scripts/publish-release.sh {lib|plugin-gradle|plugin-maven}." + } + // if the one thing being published is a plugin, make sure there aren't any unreleased + // changes in lib + val isPlugin = + changelogPushTasks[0] == ":plugin-gradle:changelogPush" || + changelogPushTasks[0] == ":plugin-maven:changelogPush" + if ( + isPlugin && + !rootSpotlessChangelog.parsedChangelog.noUnreleasedChanges() && + rootProject.findProperty("ignoreUnreleasedLib")?.toString() != "true" + ) { + throw IllegalArgumentException( + "You're going to publish ${changelogPushTasks[0]}, but there are unreleased features in lib!\n" + + "You should run :changelogPush first! Else you'll be missing out on:\n" + + "${rootSpotlessChangelog.parsedChangelog.unreleasedChanges()}\n" + + "If it's okay to miss those and link against the old ${rootSpotlessChangelog.versionLast} then " + + "add -PignoreUnreleasedLib=true" + ) + } + } + } +} diff --git a/build-logic/src/main/kotlin/spotless.error-prone.gradle.kts b/build-logic/src/main/kotlin/spotless.error-prone.gradle.kts new file mode 100644 index 0000000000..f5ba3f28f7 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.error-prone.gradle.kts @@ -0,0 +1,46 @@ +import net.ltgt.gradle.errorprone.errorprone + +plugins { + id("net.ltgt.errorprone") +} + +// error_prone_core 2.43.0+ requires Java 21+ to run (compiled with classfile version 65.0). +val isJava21Compatible = JavaVersion.current() >= JavaVersion.VERSION_21 + +tasks.withType().configureEach { + options.errorprone { + enabled = + isJava21Compatible && + (System.getProperty("error-prone")?.toBooleanGroovy() == true || + System.getenv("error-prone")?.toBooleanGroovy() == true) + // https://github.com/diffplug/spotless/issues/2745 + // https://github.com/google/error-prone/issues/5365 + disableAllWarnings = true + disable( + "AnnotateFormatMethod", // We don't want to use ErrorProne's annotations. + "FunctionalInterfaceMethodChanged", + "ImmutableEnumChecker", // We don't want to use ErrorProne's annotations. + "InlineMeSuggester", // We don't want to use ErrorProne's annotations. + "JavaxInjectOnAbstractMethod", + "OverridesJavaxInjectableMethod", + "ReturnValueIgnored", // We don't want to use ErrorProne's annotations. + ) + error( + "ReturnValueIgnored", + "SelfAssignment", + "StringJoin", + "UnnecessarilyFullyQualified", + "UnnecessaryLambda", + ) + // bug: this only happens when the file is dirty. + // might be an up2date (caching) issue, as file is currently in corrupt state. + // ForbidGradleInternal(import + excludedPaths = ".*/GradleIntegrationHarness.java" + } +} + +dependencies { + if (isJava21Compatible) { + errorprone(libs.errorprone.core) + } +} diff --git a/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts b/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts new file mode 100644 index 0000000000..11dc18cc5c --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts @@ -0,0 +1,54 @@ +plugins { + `java-library` + id("com.vanniktech.maven.publish") +} + +tasks.withType().configureEach { + options { + (this as? StandardJavadocDocletOptions)?.apply { + encoding = Charsets.UTF_8.name() + // Where it's possible to name parameters and methods clearly enough that javadoc is not + // necessary, why make the code bigger? Thus, no javadoc warnings. + addStringOption("Xdoclint:none", "-quiet") + addStringOption("Xwerror", "-quiet") + addStringOption("source", "17") + val version = project.version.toString() + val group = project.group.toString() + val artifactId = project.findProperty("POM_ARTIFACT_ID")?.toString() ?: project.name + val javadocInfo = + "

$group:$artifactId:$version by DiffPlug

" + header = javadocInfo + + linksOffline("https://docs.gradle.org/6.1.1/javadoc/", "../gradle/javadoc/gradle") + val versionLast = rootSpotlessChangelog.versionLast + linksOffline( + "https://javadoc.io/static/com.diffplug.spotless/spotless-lib/$versionLast", + "../gradle/javadoc/spotless-lib", + ) + linksOffline( + "https://javadoc.io/static/com.diffplug.spotless/spotless-lib-extra/$versionLast", + "../gradle/javadoc/spotless-lib-extra", + ) + } + } +} + +// find the project with the changelog (this project for plugins, root project for libs) +val changelogProject = if (pluginManager.hasPlugin("spotless.changelog")) project else rootProject +val changelogTasks = changelogProject.tasks + +// ensures that nothing will be built if changelogPush will end up failing +tasks.jar { + dependsOn(changelogTasks.named("changelogCheck")) +} + +// These tasks prepare publications, but Central uploads only when the build ends. +// CI uses publish-release.sh to finish publishing before a separate changelog build, +// which excludes these dependencies because the artifacts are already published. +changelogTasks.named("changelogBump") { + dependsOn(tasks.named("publishToMavenCentral")) + // if we have a Gradle plugin, we need to push it up to the plugin portal too + plugins.withId("com.gradle.plugin-publish") { + dependsOn(tasks.named("publishPlugins")) + } +} diff --git a/build-logic/src/main/kotlin/spotless.java-setup.gradle.kts b/build-logic/src/main/kotlin/spotless.java-setup.gradle.kts new file mode 100644 index 0000000000..d003a5ce18 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.java-setup.gradle.kts @@ -0,0 +1,47 @@ +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.SpotBugsTask + +plugins { + id("java") + id("com.github.spotbugs") + id("spotless.error-prone") + id("spotless.spotless-conventions") +} + +group = property("GROUP").toString() + +tasks.withType().configureEach { + options.encoding = Charsets.UTF_8.name() + options.release = libs.versions.jdk.release.map { it.toInt() } +} + +spotbugs { + ignoreFailures = false // bug free or it doesn't ship! + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = Confidence.MEDIUM + omitVisitors = + listOf( + // https://spotbugs.readthedocs.io/en/latest/detectors.html#constructorthrow + "ConstructorThrow", + // https://spotbugs.readthedocs.io/en/latest/detectors.html#findreturnref + "FindReturnRef", + ) +} + +tasks.spotbugsTest { + enabled = false +} + +tasks.withType().configureEach { + outputs.file(project.layout.buildDirectory.file("reports/spotbugs/${name}.html")) + outputs.file(project.layout.buildDirectory.file("spotbugs/auxclasspath/${name}")) + reports.create("html") { + required = true + } +} + +dependencies { + compileOnly(libs.jcip.annotations) + compileOnly(spotbugs.toolVersion.map { "com.github.spotbugs:spotbugs-annotations:$it" }) + compileOnly(libs.jsr305) +} diff --git a/build-logic/src/main/kotlin/spotless.rewrite.gradle.kts b/build-logic/src/main/kotlin/spotless.rewrite.gradle.kts new file mode 100644 index 0000000000..18f1718506 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.rewrite.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("org.openrewrite.rewrite") +} + +rewrite { + activeRecipe("com.diffplug.spotless.openrewrite.SanityCheck") + exclusion( + listOf( + "**.dirty.java", + "**FormatterProperties.java", + "**_gradle_node_plugin_example_**", + "**idea/full.clean.java", + "**package-info.java", + "**testlib/src/main/resources**", + ) + ) + isExportDatatables = true + failOnDryRunResults = true +} + +dependencies { + rewrite(libs.rewrite.recipe.migrate.java) + rewrite(libs.rewrite.recipe.static.analysis) + rewrite(libs.rewrite.recipe.third.party) +} diff --git a/build-logic/src/main/kotlin/spotless.special-tests.gradle.kts b/build-logic/src/main/kotlin/spotless.special-tests.gradle.kts new file mode 100644 index 0000000000..8353e353d9 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.special-tests.gradle.kts @@ -0,0 +1,41 @@ +import com.gradle.develocity.agent.gradle.test.DevelocityTestConfiguration + +plugins { + // this script configures the `test` task and registers Test tasks, so it needs java itself + // rather than relying on every consumer to declare it earlier in their plugins block + id("java") + id("com.adarshr.test-logger") +} + +// See com.diffplug.spotless.tag package for available JUnit 5 @Tag annotations +val special = listOf("black", "buf", "clang", "gofmt", "idea", "npm", "shfmt") + +tasks.withType().configureEach { + if (isCiServer) { + configure { + testRetry { + maxRetries = 2 + maxFailures = 10 + } + } + } + // selfie https://selfie.dev/jvm/get-started#gradle + // optional, see "Overwrite everything" there + project.findProperty("selfie")?.let { environment("selfie", it) } + // optional, improves up-to-date checking + inputs.files(fileTree("src/test") { include("**/*.ss") }) + // https://docs.gradle.org/8.8/userguide/performance.html#execute_tests_in_parallel + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) +} + +tasks.named("test") { useJUnitPlatform { excludeTags(*special.toTypedArray()) } } + +special.forEach { tag -> + tasks.register("test${tag.replaceFirstChar { it.uppercase() }}") { + useJUnitPlatform { includeTags(tag) } + val testEnv = rootProject.file("testenv.properties") + if (testEnv.exists()) { + systemProperty("testenv.properties.path", testEnv.canonicalPath) + } + } +} diff --git a/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts b/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts new file mode 100644 index 0000000000..398b576af3 --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.diffplug.spotless") +} + +// The released Spotless plugin used here only knows the legacy `com.facebook:ktfmt` coordinate. +// TODO: switch back to `libs.ktfmt` once a Spotless release supporting ktfmt 0.65+ is used. +val KTFMT_VERSION = "0.64" + +spotless { + if (project != rootProject) { + java { + ratchetFrom("origin/main") + bumpThisNumberIfACustomStepChanges(1) + licenseHeaderFile(rootProject.file("gradle/spotless.license")) + importOrderFile(rootProject.file("gradle/spotless.importorder")) + eclipse().configFile(rootProject.file("gradle/spotless.eclipseformat.xml")) + trimTrailingWhitespace() + removeUnusedImports() + formatAnnotations() + forbidWildcardImports() + forbidRegex( + "ForbidGradleInternal", + """import org\.gradle\.api\.internal\.(.*)""", + "Don't use Gradle's internal API", + ) + } + } + kotlin { + target("build-logic/src/**/*.kt") + ktfmt(KTFMT_VERSION) + } + kotlinGradle { + target("*.gradle.kts", "build-logic/*.gradle.kts", "build-logic/src/**/*.gradle.kts") + ktfmt(KTFMT_VERSION) + } + groovyGradle { + target("*.gradle", "gradle/*.gradle") + greclipse() + .configFile( + rootProject.files( + "gradle/spotless.eclipseformat.xml", + "gradle/spotless.groovyformat.prefs", + ) + ) + } + format("dotfiles") { + target(".gitignore", ".gitattributes", ".editorconfig") + leadingTabsToSpaces(2) + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/build-logic/src/main/kotlin/spotless.spotless-freshmark.gradle.kts b/build-logic/src/main/kotlin/spotless.spotless-freshmark.gradle.kts new file mode 100644 index 0000000000..cd0206aebb --- /dev/null +++ b/build-logic/src/main/kotlin/spotless.spotless-freshmark.gradle.kts @@ -0,0 +1,63 @@ +import com.diffplug.gradle.spotless.FreshMarkExtension + +plugins { + id("com.diffplug.spotless") +} + +val freshmarkSetup: (FreshMarkExtension) -> Unit = { freshmark -> + freshmark.target("*.md") + freshmark.propertiesFile(rootProject.file("gradle.properties")) + freshmark.properties { + put("yes", ":+1:") + put("no", ":white_large_square:") + } + freshmark.leadingTabsToSpaces(2) + freshmark.endWithNewline() +} + +spotless { + freshmark { + freshmarkSetup(this) + // disable ratchetFrom for freshmark, because we always want to update the readme links + ratchetFrom(null as String?) + } +} + +// if this freshmark has a changelog file, then it has version-sensitive content +pluginManager.withPlugin("com.diffplug.spotless-changelog") { + val changelog = spotlessChangelog + val versionLast = changelog.versionLast + val versionNext = changelog.versionNext + + // normally we use versionLast for our freshmark + spotless { + freshmark { + properties { + put("versionLast", versionLast) + } + } + } + + // create a freshmark apply task manually + val freshmark = FreshMarkExtension(the()) + freshmarkSetup(freshmark) + freshmark.properties { + // that uses versionNext as versionLast + put("versionLast", versionNext) + } + + val changelogBumpFreshmark = freshmark.createIndependentApplyTask("changelogBumpFreshmark") + // freshmark should run after the changelog bump + changelogBumpFreshmark.dependsOn(tasks.named("changelogBump")) + + val changelogBumpFreshmarkGitAdd = + tasks.register("changelogBumpFreshmarkGitAdd") { + // this git add should run after the freshmark + dependsOn(changelogBumpFreshmark) + commandLine("git", "add", "*.md") + } + + tasks.named("changelogPush") { + dependsOn(changelogBumpFreshmarkGitAdd) + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4d788a0a57..0000000000 --- a/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - alias(libs.plugins.equo.ide) - alias(libs.plugins.test.logger) apply false - alias(libs.plugins.spotless.changelog) apply false - alias(libs.plugins.plugin.publish) apply false - alias(libs.plugins.version.compatibility) apply false - alias(libs.plugins.nexus.publish) apply false - alias(libs.plugins.errorprone) apply false - alias(libs.plugins.rewrite) apply false - alias(libs.plugins.maven.plugin.development) apply false - alias(libs.plugins.p2deps) apply false -} - -equoIde { - branding().title('Spotless').icon(file('_images/spotless_logo.png')) - welcome().openUrl('https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md') - gradleBuildship().autoImport('.') -} - -apply from: rootProject.file('gradle/java-publish.gradle') -apply from: rootProject.file('gradle/changelog.gradle') -apply from: rootProject.file('gradle/rewrite.gradle') -allprojects { - apply from: rootProject.file('gradle/error-prone.gradle') - apply from: rootProject.file('gradle/spotless.gradle') -} -apply from: rootProject.file('gradle/spotless-freshmark.gradle') diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..6be1ceae4a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.buildconfig) apply false + alias(libs.plugins.equo.ide) + alias(libs.plugins.plugin.publish) apply false + alias(libs.plugins.version.compatibility) apply false + alias(libs.plugins.maven.plugin.development) apply false + alias(libs.plugins.p2deps) apply false + id("spotless.changelog") + id("spotless.rewrite") + id("spotless.spotless-freshmark") + id("spotless.spotless-conventions") +} + +equoIde { + branding().title("Spotless").icon(file("_images/spotless_logo.png")) + welcome().openUrl("https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md") + gradleBuildship().autoImport(".") +} diff --git a/gradle.properties b/gradle.properties index ddb7a4ab97..c0d2db2891 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,20 +4,27 @@ org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configuration-cache.parallel=true -org.gradle.tooling.parallel=true +org.gradle.kotlin.dsl.allWarningsAsErrors=true -name=spotless -description=Spotless - keep your code spotless with Gradle -org=diffplug -group=com.diffplug.spotless +########## Properties for publishing to Maven Central ########## -artifactIdLib=spotless-lib -artifactIdLibExtra=spotless-lib-extra -artifactIdTestLib=spotless-testlib +mavenCentralPublishing=true +mavenCentralAutomaticPublishing=true +mavenCentralDeploymentValidation=NONE +signAllPublications=true -# naming convention '-maven-plugin' allows mvn 'spotless:check' instead of 'spotless-plugin-maven:check' -artifactIdMaven=spotless-maven-plugin +GROUP=com.diffplug.spotless +POM_DESCRIPTION=Spotless - keep your code spotless with Gradle +POM_URL=https://github.com/diffplug/spotless +POM_SCM_URL=https://github.com/diffplug/spotless +POM_SCM_CONNECTION=scm:git:https://github.com/diffplug/spotless.git +POM_SCM_DEV_CONNECTION=scm:git:ssh:git@github.com/diffplug/spotless.git -artifactIdGradle=spotless-plugin-gradle +POM_LICENSE_NAME=The Apache Software License, Version 2.0 +POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENSE_DIST=repo +POM_DEVELOPER_ID=nedtwigg +POM_DEVELOPER_NAME=Ned Twigg +POM_DEVELOPER_EMAIL=ned.twigg@diffplug.com diff --git a/gradle/changelog.gradle b/gradle/changelog.gradle deleted file mode 100644 index a8b3a99654..0000000000 --- a/gradle/changelog.gradle +++ /dev/null @@ -1,56 +0,0 @@ -String kind -String releaseTitle -if (project.name == 'plugin-gradle') { - kind = 'gradle' - releaseTitle = 'Gradle Plugin' -} else if (project.name == 'plugin-maven') { - kind = 'maven' - releaseTitle = 'Maven Plugin' -} else { - assert project == rootProject - kind = 'lib' - releaseTitle = 'Lib' -} - -// the root project and plugins have their own changelogs -apply plugin: 'com.diffplug.spotless-changelog' -spotlessChangelog { - changelogFile 'CHANGES.md' - // need -Prelease=true in order to do a publish - appendDashSnapshotUnless_dashPrelease=true - - branch 'release' - tagPrefix "${kind}/" - commitMessage "Published ${kind}/{{version}}" // {{version}} will be replaced - tagMessage "{{changes}}" - runAfterPush "gh release create ${kind}/{{version}} --title '${releaseTitle} v{{version}}' --notes-from-tag" -} - -if (project == rootProject) { - gradle.taskGraph.whenReady { taskGraph -> - def changelogPushTasks = taskGraph.allTasks.stream() - .filter { t -> t.name == 'changelogPush' } - .map { t -> t.path } - .toList() - if (changelogPushTasks.size() > 1) { - // make sure only one changelog gets published per tag/commit - throw new IllegalArgumentException("Run changelogPush one at a time:\n" + changelogPushTasks.join('\n')) - } else if (changelogPushTasks.size() == 1) { - boolean isPlugin = (changelogPushTasks[0] == ':plugin-gradle:changelogPush') || (changelogPushTasks[1] == ':plugin-maven:changelogPush') - // if the one thing being published is a plugin - if (isPlugin) { - // make sure there aren't any unreleased changes in lib - if (!rootProject.spotlessChangelog.parsedChangelog.noUnreleasedChanges()) { - if (!("true".equals(rootProject.findProperty('ignoreUnreleasedLib')))) { - throw new IllegalArgumentException( - "You're going to publish ${changelogPushTasks[0]}, but there are unreleased features in lib!\n" + - "You should run :changelogPush first! Else you'll be missing out on:\n" + - "${rootProject.spotlessChangelog.parsedChangelog.unreleasedChanges()}\n" + - "If it's okay to miss those and link against the old ${rootProject.spotlessChangelog.versionLast} then " + - "add -PignoreUnreleasedLib=true") - } - } - } - } - } -} diff --git a/gradle/error-prone.gradle b/gradle/error-prone.gradle deleted file mode 100644 index 11cfd2015b..0000000000 --- a/gradle/error-prone.gradle +++ /dev/null @@ -1,34 +0,0 @@ -import static java.lang.System.getenv - -apply plugin: 'net.ltgt.errorprone' - -tasks.withType(JavaCompile).configureEach { - options.errorprone { - getenv('error-prone')?.toBoolean() ? enable() : disable() - disableAllWarnings = true // https://github.com/diffplug/spotless/issues/2745 https://github.com/google/error-prone/issues/5365 - disable( - 'AnnotateFormatMethod', // We don`t want to use ErrorProne's annotations. - 'FunctionalInterfaceMethodChanged', - 'ImmutableEnumChecker', // We don`t want to use ErrorProne's annotations. - 'InlineMeSuggester', // We don`t want to use ErrorProne's annotations. - 'JavaxInjectOnAbstractMethod', - 'OverridesJavaxInjectableMethod', - 'ReturnValueIgnored', // We don`t want to use ErrorProne's annotations. - ) - error( - 'ReturnValueIgnored', - 'SelfAssignment', - 'StringJoin', - 'UnnecessarilyFullyQualified', - 'UnnecessaryLambda', - ) - // bug: this only happens when the file is dirty. - // might be an up2date (caching) issue, as file is currently in corrupt state. - // ForbidGradleInternal(import - excludedPaths.set('.*/GradleIntegrationHarness.java') - } -} - -dependencies { - errorprone libs.errorprone.core -} diff --git a/gradle/java-publish.gradle b/gradle/java-publish.gradle deleted file mode 100644 index 80931905ef..0000000000 --- a/gradle/java-publish.gradle +++ /dev/null @@ -1,210 +0,0 @@ -import java.nio.charset.StandardCharsets - -def decode64(String varName) { - String envValue = System.env[varName] - if (envValue == null) { - return "" - } else { - return new String(envValue.decodeBase64(), "UTF-8") - } -} - -if (project.parent == null) { - group = 'com.diffplug.spotless' - def pass = System.env['ORG_GRADLE_PROJECT_nexus_pass64'] - if (pass != null) { - pass = pass.decodeBase64() - } - // it's the root project - apply plugin: 'io.github.gradle-nexus.publish-plugin' - nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) - snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) - username = System.env['ORG_GRADLE_PROJECT_nexus_user'] - password = decode64('ORG_GRADLE_PROJECT_nexus_pass64') - } - } - } - def initTask = tasks.named('initializeSonatypeStagingRepository') - allprojects { - initTask.configure { - shouldRunAfter(tasks.withType(Sign)) - } - } - return -} - -/////////// -// MAVEN // -/////////// -java { - withJavadocJar() - withSourcesJar() -} - -tasks.named('sourcesJar') { - dependsOn 'jar' -} - -apply plugin: 'maven-publish' -apply plugin: 'signing' - -// Where it's possible to name parameters and methods clearly enough -// that javadoc is not necessary, why make the code bigger? -// -// Thus, no javadoc warnings. -javadoc { - options.addStringOption('Xdoclint:none', '-quiet') - options.addStringOption('Xwerror', '-quiet') -} -// make sure bad javadoc breaks the build -tasks.named('check') { - dependsOn tasks.named('javadoc') -} - -// use markdown in javadoc -def makeLink = { url, text -> "${text}" } -def javadocInfo = '

' + makeLink("https://github.com/${org}/${name}", "${group}:${project.ext.artifactId}:${version}") + - ' by ' + makeLink('https://www.diffplug.com', 'DiffPlug') + '

' - -String dotdotGradle = project.name.startsWith('eclipse-') ? '../../gradle' : '../gradle' -javadoc { - options.encoding = 'UTF-8' - // Where it's possible to name parameters and methods clearly enough - // that javadoc is not necessary, why make the code bigger? - // - // Thus, no javadoc warnings. - options.addStringOption('Xdoclint:none', '-quiet') - options.addStringOption('source', '17') - // setup the header - options.header javadocInfo - // setup links - options.linksOffline('https://docs.gradle.org/6.1.1/javadoc/', "${dotdotGradle}/javadoc/gradle") - - // links to javadoc from the other versions - options.linksOffline("https://javadoc.io/static/com.diffplug.spotless/spotless-lib/${rootProject.spotlessChangelog.versionLast}", "${dotdotGradle}/javadoc/spotless-lib") - options.linksOffline("https://javadoc.io/static/com.diffplug.spotless/spotless-lib-extra/${rootProject.spotlessChangelog.versionLast}", "${dotdotGradle}/javadoc/spotless-lib-extra") -} - -//////////////// -// PUBLISHING // -//////////////// -final MAVEN_PLUGIN_ARTIFACT_NAME = 'spotless-maven-plugin' -boolean isExt = project.name.startsWith('eclipse-') -boolean isPluginMaven = project.ext.artifactId == 'spotless-maven-plugin' - -model { - publishing { - publications { - pluginMaven(MavenPublication) { - if (project.ext.artifactId != 'spotless-plugin-gradle') { - from components.java - } - - groupId = project.group - artifactId = project.ext.artifactId - version = project.version - - def projectExtArtifactId = project.ext.artifactId - def projectDescription = project.description - def projectOrg = project.org - def rootProjectName = rootProject.name - - pom.withXml { - // add MavenCentral requirements to the POM - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name projectExtArtifactId - description projectDescription - url "https://github.com/${projectOrg}/${rootProjectName}" - scm { - url "https://github.com/${projectOrg}/${rootProjectName}" - connection "scm:git:https://github.com/${projectOrg}/${rootProjectName}.git" - developerConnection "scm:git:ssh:git@github.com/${projectOrg}/${rootProjectName}.git" - } - licenses { - if (isExt) { - license { - name 'Eclipse Public License - v 1.0' - url 'https://www.eclipse.org/legal/epl-v10.html' - distribution 'repo' - } - } else { - license { - name 'The Apache Software License, Version 2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - } - if (isPluginMaven) { - // Maven plugin required Maven 3.1.0+ to run - prerequisites { maven '3.1.0' } - } - developers { - if (isExt) { - project.ext.developers.each { extId, extValues -> - developer { - id extId - name extValues['name'] - email extValues['email'] - } - } - } else { - if (isPluginMaven) { - developer { - id 'lutovich' - name 'Konstantin Lutovich' - email 'konstantin.lutovich@neotechnology.com' - } - } - developer { - id 'nedtwigg' - name 'Ned Twigg' - email 'ned.twigg@diffplug.com' - } - } - } - } - } - } - } - } -} - -if (System.env['JITPACK'] == 'true' || version.endsWith('-SNAPSHOT')) { - signing { - setRequired(false) - } -} else { - signing { - String gpg_key = decode64('ORG_GRADLE_PROJECT_gpg_key64') - useInMemoryPgpKeys('0x4272C851', gpg_key, System.env['ORG_GRADLE_PROJECT_gpg_passphrase']) - sign(publishing.publications) - } - - // find the project with the changelog (this project for plugins, root project for libs) - def changelogTasks = (tasks.names.contains('changelogBump') ? project : rootProject).tasks - - // ensures that nothing will be built if changelogPush will end up failing - tasks.named('jar').configure { - dependsOn changelogTasks.named('changelogCheck') - } - // ensures that changelog bump and push only happens if the publish was successful - def thisProj = project - changelogTasks.named('changelogBump').configure { - dependsOn ":${thisProj.path}:publishPluginMavenPublicationToSonatypeRepository" - dependsOn ":closeAndReleaseSonatypeStagingRepository" - // if we have a Gradle plugin, we need to push it up to the plugin portal too - if (thisProj.tasks.names.contains('publishPlugins')) { - dependsOn thisProj.tasks.named('publishPlugins') - } - } -} - -tasks.withType(AbstractArchiveTask).configureEach { - preserveFileTimestamps = false - reproducibleFileOrder = true -} diff --git a/gradle/java-setup.gradle b/gradle/java-setup.gradle deleted file mode 100644 index d73cde3f32..0000000000 --- a/gradle/java-setup.gradle +++ /dev/null @@ -1,43 +0,0 @@ -////////// -// JAVA // -////////// - -// setup java -apply plugin: 'java' -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' - options.release = libs.versions.jdk.release.get().toInteger() -} - -////////////// -// SPOTBUGS // -////////////// -apply plugin: 'com.github.spotbugs' -spotbugs { - ignoreFailures = false // bug free or it doesn't ship! - // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). - reportLevel = com.github.spotbugs.snom.Confidence.valueOf('MEDIUM') - omitVisitors = [ - // https://spotbugs.readthedocs.io/en/latest/detectors.html#constructorthrow - 'ConstructorThrow', - // https://spotbugs.readthedocs.io/en/latest/detectors.html#findreturnref - 'FindReturnRef', - ] -} -tasks.named('spotbugsTest') { - enabled = false -} - -tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach { - outputs.file(project.layout.buildDirectory.file("reports/spotbugs/${it.name}.html")) - outputs.file(project.layout.buildDirectory.file("spotbugs/auxclasspath/${it.name}")) - reports { - html.enabled = true - } -} - -dependencies { - compileOnly libs.jcip.annotations - compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" - compileOnly libs.jsr305 -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8807b1dd4..33a3bea2cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ jdk-release = "17" durian = "1.2.0" selfie = "3.1.1" -jackson = "2.20.1" -maven-api = "3.0" +jackson = "2.22.3" ktlint = "1.8.0" +javaparser = "3.28.2" [libraries] durian-core = { module = "com.diffplug.durian:durian-core", version.ref = "durian" } @@ -20,60 +20,66 @@ solstice = "dev.equo.ide:solstice:1.8.2" jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } -maven-plugin-api = { module = "org.apache.maven:maven-plugin-api", version.ref = "maven-api" } -maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven-api" } -maven-core = { module = "org.apache.maven:maven-core", version.ref = "maven-api" } - ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" } ktlint-ruleset-standard = { module = "com.pinterest.ktlint:ktlint-ruleset-standard", version.ref = "ktlint" } -jgit = "org.eclipse.jgit:org.eclipse.jgit:7.7.0.202606012155-r" -junit-jupiter = "org.junit.jupiter:junit-jupiter:6.1.1" +javaparser-core = { module = "com.github.javaparser:javaparser-core", version.ref = "javaparser" } +javaparser-symbol-solver-core = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "javaparser" } + +palantir-java-format = "com.palantir.javaformat:palantir-java-format:2.102.0" + +jgit = "org.eclipse.jgit:org.eclipse.jgit:7.8.0.202609011348-r" +junit-jupiter = "org.junit.jupiter:junit-jupiter:6.1.3" assertj-core = "org.assertj:assertj-core:3.27.7" -mockito-core = "org.mockito:mockito-core:5.23.0" -osgi = "org.eclipse.platform:org.eclipse.osgi:3.24.200" -slf4j-api = "org.slf4j:slf4j-api:2.0.18" +mockito-core = "org.mockito:mockito-core:5.24.0" +osgi = "org.eclipse.platform:org.eclipse.osgi:3.24.300" +slf4j-api = "org.slf4j:slf4j-api:2.0.20" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" jcip-annotations = "net.jcip:jcip-annotations:1.0" jakarta-annotation-api = "jakarta.annotation:jakarta.annotation-api:3.0.0" -aether-api = "org.eclipse.aether:aether-api:1.1.0" -plexus-resources = "org.codehaus.plexus:plexus-resources:1.3.1" -plexus-utils = "org.codehaus.plexus:plexus-utils:4.0.3" -plexus-build-api = "org.sonatype.plexus:plexus-build-api:0.0.7" +javax-inject = "javax.inject:javax.inject:1" +plexus-resources = "org.codehaus.plexus:plexus-resources:1.4.0" +plexus-utils = "org.codehaus.plexus:plexus-utils:4.1.0" concurrent-trees = "com.googlecode.concurrent-trees:concurrent-trees:2.6.1" -owasp-encoder = "org.owasp.encoder:encoder:1.4.0" +owasp-encoder = "org.owasp.encoder:encoder:1.5.0" mustache-compiler = "com.github.spullara.mustache.java:compiler:0.9.14" -cleanthat-java = "io.github.solven-eu.cleanthat:java:2.24" -diktat-rules = "org.cqfn.diktat:diktat-rules:1.2.5" +adocfmt = "org.drjekyll:adocfmt:0.3.1" +antlr4-formatter = "com.khubla.antlr4formatter:antlr4-formatter:1.2.1" +cleanthat-java = "io.github.solven-eu.cleanthat:java:2.26" +cool-rdf-formatter = "cool.rdf:cool-rdf-formatter:2.0.1" diktat-runner = "com.saveourtool.diktat:diktat-runner:2.0.0" flexmark-all = "com.vladsch.flexmark:flexmark-all:0.64.8" -gherkin-utils = "io.cucumber:gherkin-utils:10.0.0" -google-java-format = "com.google.googlejavaformat:google-java-format:1.28.0" +freshmark = "com.diffplug.freshmark:freshmark:1.3.1" +cucumber-gherkin = "io.cucumber:gherkin:42.0.1" +gherkin-utils = "io.cucumber:gherkin-utils:12.0.2" +google-java-format = "com.google.googlejavaformat:google-java-format:1.36.1" gson = "com.google.code.gson:gson:2.14.0" -javaparser-symbol-solver-core = "com.github.javaparser:javaparser-symbol-solver-core:3.27.1" -ktfmt = "com.facebook:ktfmt:0.63" -palantir-java-format = "com.palantir.javaformat:palantir-java-format:1.1.0" -scalafmt-core = "org.scalameta:scalafmt-core_2.13:3.8.1" +json-simple = "org.json:json:20210307" +ktfmt = "org.jetbrains.kotlinx:ktfmt:0.65" +prince-of-space-core = "io.github.agustafson.princeofspace:prince-of-space-core:2.2.0" +scalafmt-core = "org.scalameta:scalafmt-core_2.13:3.11.5" sortpom-sorter = "com.github.ekryd.sortpom:sortpom-sorter:4.0.0" -tabletest-formatter-core = "org.tabletest:tabletest-formatter-core:1.1.1" +tabletest-formatter-core = "org.tabletest:tabletest-formatter-core:1.1.2" zjsonpatch = "com.flipkart.zjsonpatch:zjsonpatch:0.4.16" -rewrite-recipe-migrate-java = "org.openrewrite.recipe:rewrite-migrate-java:3.36.0" -rewrite-recipe-static-analysis = "org.openrewrite.recipe:rewrite-static-analysis:2.36.0" -rewrite-recipe-third-party = "org.openrewrite.recipe:rewrite-third-party:0.40.1" +rewrite-recipe-migrate-java = "org.openrewrite.recipe:rewrite-migrate-java:3.42.1" +rewrite-recipe-static-analysis = "org.openrewrite.recipe:rewrite-static-analysis:2.41.1" +rewrite-recipe-third-party = "org.openrewrite.recipe:rewrite-third-party:0.45.0" -errorprone-core = "com.google.errorprone:error_prone_core:2.42.0" +errorprone-core = "com.google.errorprone:error_prone_core:2.50.0" [plugins] +buildconfig = "com.github.gmazzo.buildconfig:6.1.2" test-logger = "com.adarshr.test-logger:4.0.0" -spotless = "com.diffplug.spotless:8.7.0" +spotless = "com.diffplug.spotless:8.10.3" spotless-changelog = "com.diffplug.spotless-changelog:3.1.2" -spotbugs = "com.github.spotbugs:6.5.8" -plugin-publish = "com.gradle.plugin-publish:2.1.1" +spotbugs = "com.github.spotbugs:6.5.12" +plugin-publish = "com.gradle.plugin-publish:2.2.1" equo-ide = "dev.equo.ide:1.7.8" version-compatibility = "io.github.davidburstrom.version-compatibility:0.5.0" p2deps = "dev.equo.p2deps:1.7.8" -nexus-publish = "io.github.gradle-nexus.publish-plugin:2.0.0" -errorprone = "net.ltgt.errorprone:5.1.0" -rewrite = "org.openrewrite.rewrite:7.34.0" +maven-publish = "com.vanniktech.maven.publish:0.37.0" +errorprone = "net.ltgt.errorprone:5.1.1" +rewrite = "org.openrewrite.rewrite:7.39.0" maven-plugin-development = "org.gradlex.maven-plugin-development:1.0.3" +develocity = "com.gradle.develocity:4.6.0" diff --git a/gradle/pinned-libs.versions.toml b/gradle/pinned-libs.versions.toml new file mode 100644 index 0000000000..9f645d1161 --- /dev/null +++ b/gradle/pinned-libs.versions.toml @@ -0,0 +1,17 @@ +# This version catalog contains pinned/locked dependencies (e.g. for backward-compatibility +# compile targets or fixed build tool APIs). Automated dependency updates (like Renovate) +# are explicitly disabled for this file. + +[versions] +maven = "3.0" + +[libraries] +maven-plugin-api = { module = "org.apache.maven:maven-plugin-api", version.ref = "maven" } +maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven" } +maven-core = { module = "org.apache.maven:maven-core", version.ref = "maven" } + +palantir-java-format = "com.palantir.javaformat:palantir-java-format:1.1.0" +google-java-format = "com.google.googlejavaformat:google-java-format:1.28.0" +diktat-rules = "org.cqfn.diktat:diktat-rules:1.2.5" +aether-api = "org.eclipse.aether:aether-api:1.1.0" +plexus-build-api = "org.sonatype.plexus:plexus-build-api:0.0.7" diff --git a/gradle/rewrite.gradle b/gradle/rewrite.gradle deleted file mode 100644 index 7f548130e5..0000000000 --- a/gradle/rewrite.gradle +++ /dev/null @@ -1,27 +0,0 @@ -apply plugin: 'org.openrewrite.rewrite' -rewrite { - activeRecipe('com.diffplug.spotless.openrewrite.SanityCheck') - exclusions.addAll( - '**.dirty.java', - '**FormatterProperties.java', - '**_gradle_node_plugin_example_**', - '**gradle/changelog.gradle', - '**gradle/java-publish.gradle', - '**idea/full.clean.java', - '**java-setup.gradle', - '**lib-extra/build.gradle', - '**lib/build.gradle', - '**package-info.java', - '**plugin-maven/build.gradle', - '**settings.gradle', - '**special-tests.gradle', - '**testlib/src/main/resources**' - ) - exportDatatables = true - failOnDryRunResults = true -} -dependencies { - rewrite libs.rewrite.recipe.migrate.java - rewrite libs.rewrite.recipe.static.analysis - rewrite libs.rewrite.recipe.third.party -} diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle deleted file mode 100644 index c4c5544378..0000000000 --- a/gradle/special-tests.gradle +++ /dev/null @@ -1,48 +0,0 @@ -apply plugin: 'com.adarshr.test-logger' - -// See com.diffplug.spotless.tag package for available JUnit 5 @Tag annotations -def special = [ - 'black', - 'buf', - 'clang', - 'gofmt', - 'idea', - 'npm', - 'shfmt' -] - -boolean isCiServer = System.getenv().containsKey("CI") -tasks.withType(Test).configureEach { - if (isCiServer) { - develocity { - testRetry { - maxRetries = 2 - maxFailures = 10 - } - } - } - // selfie https://selfie.dev/jvm/get-started#gradle - environment project.properties.subMap([ - "selfie" - ]) // optional, see "Overwrite everything" below - inputs.files(fileTree("src/test") { - // optional, improves up-to-date checking - include "**/*.ss" - }) - // https://docs.gradle.org/8.8/userguide/performance.html#execute_tests_in_parallel - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 -} -tasks.named('test').configure { - useJUnitPlatform { - excludeTags special as String[] - } -} -special.forEach { tag -> - tasks.register("test${tag.capitalize()}", Test) { - useJUnitPlatform { includeTags tag } - if (rootProject.file('testenv.properties').exists()) { - systemProperty 'testenv.properties.path', rootProject.file('testenv.properties').canonicalPath - } - } -} - diff --git a/gradle/spotless-freshmark.gradle b/gradle/spotless-freshmark.gradle deleted file mode 100644 index d4f72a3344..0000000000 --- a/gradle/spotless-freshmark.gradle +++ /dev/null @@ -1,59 +0,0 @@ - -apply plugin: 'com.diffplug.spotless' - -import com.diffplug.gradle.spotless.FreshMarkExtension - -Action freshmarkSetup = { - it.target '*.md' - it.propertiesFile(rootProject.file('gradle.properties')) - it.properties { - it.put('yes', ':+1:') - it.put('no', ':white_large_square:') - } - it.leadingTabsToSpaces(2) - it.endWithNewline() -} - -spotless { - freshmark { - freshmarkSetup.execute(it) - // disable ratchetFrom for freshmark, because we always want to update the readme links - ratchetFrom null - } -} - -def versionLast = spotlessChangelog.versionLast -def versionNext = spotlessChangelog.versionNext - -// if this freshmark has a changelog file, then it has version-sensitive content -if (tasks.names.contains('changelogCheck')) { - // normally we use versionLast for our freshmark - spotless { - freshmark { - properties { - it.put('versionLast', versionLast) - } - } - } - - // create a freshmark apply task manually - FreshMarkExtension freshmark = new FreshMarkExtension(spotless) - freshmarkSetup.execute(freshmark) - freshmark.properties { - // that uses versionNext as versionLast - it.put('versionLast', versionNext) - } - def changelogBumpFreshmark = freshmark.createIndependentApplyTask('changelogBumpFreshmark') - // freshmark should run after the changelog bump - changelogBumpFreshmark.dependsOn tasks.named('changelogBump') - - def changelogBumpFreshmarkGitAdd = tasks.register('changelogBumpFreshmarkGitAdd', Exec) { - // this git add should run after the freshmark - dependsOn(changelogBumpFreshmark) - // do the git add - commandLine 'git', 'add' , '*.md' - } - tasks.named('changelogPush').configure { - dependsOn changelogBumpFreshmarkGitAdd - } -} diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle deleted file mode 100644 index 4202d3e62b..0000000000 --- a/gradle/spotless.gradle +++ /dev/null @@ -1,28 +0,0 @@ -apply plugin: 'com.diffplug.spotless' -spotless { - if (project != rootProject) { - // the rootProject doesn't have any java - java { - ratchetFrom 'origin/main' - bumpThisNumberIfACustomStepChanges(1) - licenseHeaderFile rootProject.file('gradle/spotless.license') - importOrderFile rootProject.file('gradle/spotless.importorder') - eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml') - trimTrailingWhitespace() - removeUnusedImports() - formatAnnotations() - forbidWildcardImports() - forbidRegex('ForbidGradleInternal', 'import org\\.gradle\\.api\\.internal\\.(.*)', "Don't use Gradle's internal API") - } - } - groovyGradle { - target '*.gradle', 'gradle/*.gradle' - greclipse().configFile rootProject.files('gradle/spotless.eclipseformat.xml', 'gradle/spotless.groovyformat.prefs') - } - format 'dotfiles', { - target '.gitignore', '.gitattributes', '.editorconfig' - leadingTabsToSpaces(2) - trimTrailingWhitespace() - endWithNewline() - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc60f..9fa7f28828 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8e61ef1259..a78c88e539 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,10 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 739907dfd1..249efbb032 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3ab8e..3185a43f78 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,39 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,35 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index adb3fe10c8..0000000000 --- a/jitpack.yml +++ /dev/null @@ -1,2 +0,0 @@ -jdk: - - openjdk11 diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle deleted file mode 100644 index 99e8b233b2..0000000000 --- a/lib-extra/build.gradle +++ /dev/null @@ -1,104 +0,0 @@ -plugins { - id 'java-library' - id 'dev.equo.p2deps' -} -ext.artifactId = project.artifactIdLibExtra -version = rootProject.spotlessChangelog.versionNext -apply from: rootProject.file('gradle/java-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -dependencies { - api projects.lib - // misc useful utilities - implementation libs.durian.core - implementation libs.durian.collect - // needed by GitAttributesLineEndings - implementation libs.jgit - implementation libs.concurrent.trees - // for eclipse - implementation libs.solstice - // the osgi dep is included in solstice, but it has some CVE's against it. - // 3.18.500 is the oldest, most-compatible version with no CVE's - // https://central.sonatype.com/artifact/org.eclipse.platform/org.eclipse.osgi/versions - implementation libs.osgi - - // testing - testImplementation projects.testlib - testImplementation libs.durian.io - testImplementation libs.jsr305 - testImplementation libs.junit.jupiter - testImplementation libs.assertj.core - testImplementation libs.durian.testlib - testRuntimeOnly "org.junit.platform:junit-platform-launcher" -} - -spotless { - java { - replaceRegex 'enforceSolsticeVersion', '"dev.equo.ide:solstice:(.*)"', "\"${libs.solstice.get().toString()}\"" - } -} - -apply from: rootProject.file('gradle/special-tests.gradle') -tasks.withType(Test).configureEach { - // needed for EclipseCdtFormatterStepTest - jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' -} - -def NEEDS_P2_DEPS = [ - // (alphabetic order please) - 'cdt', - 'groovy', - 'jdt' -] -for (needsP2 in NEEDS_P2_DEPS) { - sourceSets.register(needsP2) { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - java {} - } - dependencies { - add("${needsP2}CompileOnly", libs.solstice) - } -} - -def jar = tasks.named('jar', Jar) { - for (needsP2 in NEEDS_P2_DEPS) { - from sourceSets.named(needsP2).map { it.output.classesDirs } - } -} - -tasks.withType(Test).configureEach { - dependsOn jar - classpath += jar.get().outputs.files - // P2 tests use a process-level file lock; parallel forks race for it. - // In-JVM synchronization in TestP2Provisioner is enough, so one fork is sufficient. - maxParallelForks = 1 -} - -apply plugin: 'dev.equo.p2deps' -p2deps { - into 'cdtCompileOnly', { - p2repo 'https://download.eclipse.org/eclipse/updates/4.26/' - p2repo 'https://download.eclipse.org/tools/cdt/releases/11.0/' - install 'org.eclipse.cdt.core' - } - into 'groovyCompileOnly', { - p2repo 'https://download.eclipse.org/eclipse/updates/4.26/' - p2repo 'https://groovy.jfrog.io/artifactory/plugins-release/org/codehaus/groovy/groovy-eclipse-integration/4.8.0/e4.26/' - install 'org.codehaus.groovy.eclipse.refactoring' - install 'org.codehaus.groovy.eclipse.core' - install 'org.eclipse.jdt.groovy.core' - install 'org.codehaus.groovy' - } - into 'jdtCompileOnly', { - p2repo 'https://download.eclipse.org/eclipse/updates/4.26/' - install 'org.eclipse.jdt.core' - } -} - -// we'll hold the core lib to a high standard -spotbugs { - // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). - reportLevel = com.github.spotbugs.snom.Confidence.valueOf('LOW') - excludeFilter = file("spotbugs-exclude.xml") -} diff --git a/lib-extra/build.gradle.kts b/lib-extra/build.gradle.kts new file mode 100644 index 0000000000..63ef257d6e --- /dev/null +++ b/lib-extra/build.gradle.kts @@ -0,0 +1,131 @@ +import com.github.spotbugs.snom.Confidence + +plugins { + `java-library` + alias(libs.plugins.p2deps) + id("spotless.java-setup") + id("spotless.java-publish") + id("spotless.special-tests") +} + +version = spotlessChangelog.versionNext + +dependencies { + api(projects.lib) + // misc useful utilities + implementation(libs.durian.core) + implementation(libs.durian.collect) + // needed by GitAttributesLineEndings + implementation(libs.jgit) + implementation(libs.concurrent.trees) + // for eclipse + implementation(libs.solstice) + // the osgi dep is included in solstice, but it has some CVE's against it. + // 3.18.500 is the oldest, most-compatible version with no CVE's + // https://central.sonatype.com/artifact/org.eclipse.platform/org.eclipse.osgi/versions + implementation(libs.osgi) + + // testing + testImplementation(projects.testlib) + testImplementation(libs.durian.io) + testImplementation(libs.jsr305) + testImplementation(libs.junit.jupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.durian.testlib) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +spotless { + java { + replaceRegex( + "enforceSolsticeVersion", + "\"dev.equo.ide:solstice:(.*)\"", + "\"${libs.solstice.get()}\"", + ) + } +} + +val needsP2Deps = + listOf( + // (alphabetic order please) + "cdt", + "groovy", + "jdt", + ) + +val mainSourceSet = sourceSets.named("main") + +for (needsP2 in needsP2Deps) { + sourceSets.register(needsP2) { + compileClasspath += mainSourceSet.get().output + runtimeClasspath += mainSourceSet.get().output + java {} + } + dependencies { add("${needsP2}CompileOnly", libs.solstice) } +} + +tasks.jar { + for (needsP2 in needsP2Deps) { + from(sourceSets.named(needsP2).map { it.output.classesDirs }) + } +} + +tasks.withType().configureEach { + // needed for EclipseCdtFormatterStepTest + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + dependsOn(tasks.jar) + classpath += tasks.jar.get().outputs.files + // P2 tests use a process-level file lock; parallel forks race for it. + // In-JVM synchronization in TestP2Provisioner is enough, so one fork is sufficient. + maxParallelForks = 1 +} + +p2deps { + into("cdtCompileOnly") { + p2repo("https://download.eclipse.org/eclipse/updates/4.26/") + p2repo("https://download.eclipse.org/tools/cdt/releases/11.0/") + install("org.eclipse.cdt.core") + } + into("groovyCompileOnly") { + p2repo("https://download.eclipse.org/eclipse/updates/4.26/") + p2repo( + "https://groovy.jfrog.io/artifactory/plugins-release/org/codehaus/groovy/groovy-eclipse-integration/4.8.0/e4.26/" + ) + install("org.codehaus.groovy.eclipse.refactoring") + install("org.codehaus.groovy.eclipse.core") + install("org.eclipse.jdt.groovy.core") + install("org.codehaus.groovy") + } + into("jdtCompileOnly") { + p2repo("https://download.eclipse.org/eclipse/updates/4.26/") + install("org.eclipse.jdt.core") + } +} + +// Embedded Eclipse JDT lockfiles, see EclipseJdtLockfileMetadataTool. `verify` runs in CI so the +// lockfiles cannot silently drift from Eclipse's P2 metadata; `update` is run by hand whenever the +// default version moves. Both hit the Eclipse update sites, so neither is wired into `build`. +fun eclipseJdtLockfileTool(entrypoint: String): Action = Action { + group = "verification" + classpath = sourceSets.named("test").get().runtimeClasspath + mainClass = "com.diffplug.spotless.extra.java.EclipseJdtLockfileMetadataTool\$$entrypoint" + workingDir = rootProject.projectDir + outputs.upToDateWhen { false } +} + +tasks.register("verifyEclipseJdtLockfiles") { + eclipseJdtLockfileTool("Verify").execute(this) + description = "Checks the embedded Eclipse JDT lockfiles against Eclipse P2 metadata." +} + +tasks.register("updateEclipseJdtLockfiles") { + eclipseJdtLockfileTool("Update").execute(this) + description = "Regenerates the embedded Eclipse JDT lockfiles from Eclipse P2 metadata." +} + +// we'll hold the core lib to a high standard +spotbugs { + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = Confidence.LOW + excludeFilter = file("spotbugs-exclude.xml") +} diff --git a/lib-extra/gradle.properties b/lib-extra/gradle.properties new file mode 100644 index 0000000000..40324fece5 --- /dev/null +++ b/lib-extra/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=spotless-lib-extra +POM_NAME=spotless-lib-extra diff --git a/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java b/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java index 170588e954..861729e798 100644 --- a/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java +++ b/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java @@ -17,7 +17,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.lang.reflect.Proxy; +import java.net.URISyntaxException; +import java.net.URLClassLoader; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; @@ -42,6 +46,8 @@ import org.eclipse.osgi.internal.location.EquinoxLocations; import org.eclipse.text.edits.TextEdit; import org.osgi.framework.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import dev.equo.solstice.NestedJars; import dev.equo.solstice.ShimIdeBootstrapServices; @@ -50,15 +56,50 @@ /** Spotless-Formatter step which calls out to the Groovy-Eclipse formatter. */ public class GrEclipseFormatterStepImpl { + + private static final Logger LOGGER = LoggerFactory.getLogger(GrEclipseFormatterStepImpl.class); + + private static Logger warnAsInfo(Logger delegate) { + return (Logger) Proxy.newProxyInstance( + Logger.class.getClassLoader(), + new Class[]{Logger.class}, + (proxy, method, args) -> { + String name = method.getName(); + + if ("warn".equals(name)) { + return Logger.class + .getMethod("info", method.getParameterTypes()) + .invoke(delegate, args); + } + if ("isWarnEnabled".equals(name)) { + return Logger.class + .getMethod("isInfoEnabled", method.getParameterTypes()) + .invoke(delegate, args); + } + + return method.invoke(delegate, args); + }); + } + static { NestedJars.setToWarnOnly(); - NestedJars.onClassPath().confirmAllNestedJarsArePresentOnClasspath(CacheLocations.p2nestedJars()); + NestedJars.onClassPath().confirmAllNestedJarsArePresentOnClasspath(nestedJarFolder()); + try { var solstice = Solstice.findBundlesOnClasspath(); - solstice.warnAndModifyManifestsToFix(); - var props = Map.of("osgi.nl", "en_US", + + // Missing OSGi requirements are expected in the minimal Eclipse runtime + // used by the formatter. Solstice removes them before starting it. + solstice.warnAndModifyManifestsToFix(warnAsInfo(LOGGER)); + + var props = Map.of( + "osgi.nl", "en_US", Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT, - EquinoxLocations.PROP_INSTANCE_AREA, Files.createTempDirectory("spotless-groovy").toAbsolutePath().toString()); + EquinoxLocations.PROP_INSTANCE_AREA, + Files.createTempDirectory("spotless-groovy-instance").toAbsolutePath().toString(), + EquinoxLocations.PROP_CONFIG_AREA, + Files.createTempDirectory("spotless-groovy-config").toAbsolutePath().toString()); + solstice.openShim(props); ShimIdeBootstrapServices.apply(props, solstice.getContext()); solstice.start("org.apache.felix.scr"); @@ -69,6 +110,27 @@ public class GrEclipseFormatterStepImpl { } } + private static File nestedJarFolder() { + ClassLoader classLoader = GrEclipseFormatterStepImpl.class.getClassLoader(); + if (classLoader instanceof URLClassLoader urlClassLoader) { + for (var url : urlClassLoader.getURLs()) { + if (!"file".equals(url.getProtocol())) { + continue; + } + try { + File file = new File(url.toURI()); + File parent = file.getParentFile(); + if (parent != null && NestedJars.DIR.equals(parent.getName())) { + return parent; + } + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } + return CacheLocations.p2nestedJars(); + } + /** * Groovy compiler problems can be ignored. *

diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java index 2c3d1a5a6c..6307e5395f 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,9 +55,11 @@ public class EclipseBasedStepBuilder { * the fixed versions for the formatter and its transitive dependencies. * Each line is either a comment starting with {@code #} or corresponds to the format * {@code :[:packaging][:classifier]:} + *

+ * Shared with {@link EquoBasedStepBuilder}, which follows the same lockfile layout. *

*/ - private static final String ECLIPSE_FORMATTER_RESOURCES = EclipseBasedStepBuilder.class.getPackage().getName().replace('.', '/'); + static final String ECLIPSE_FORMATTER_RESOURCES = EclipseBasedStepBuilder.class.getPackage().getName().replace('.', '/'); private List dependencies = new ArrayList<>(); private Iterable settingsFiles = new ArrayList<>(); diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java index f9748217be..9c17ca9163 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java @@ -18,7 +18,10 @@ import static java.util.stream.Collectors.toMap; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -28,6 +31,7 @@ import javax.annotation.Nullable; +import com.diffplug.common.base.Errors; import com.diffplug.common.collect.ImmutableMap; import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; @@ -122,17 +126,67 @@ protected void addPlatformRepo(P2Model model, String version) { /** Returns the FormatterStep (whose state will be calculated lazily). */ public FormatterStep build() { var roundtrippableState = new EquoStep(formatterVersion, settingProperties, settingXml, FileSignature.promise(settingsFiles), JarState.promise(() -> { + List lockfileDependencies = readEmbeddedLockfileDependencies(formatterVersion); + if (lockfileDependencies != null) { + return JarState.withoutTransitives(lockfileDependencies, mavenProvisioner); + } P2Model model = createModelWithMirrors(); P2ModelWrapper modelWrapper = P2ModelWrapper.wrap(model); - List classpath = p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory).stream() - // work around https://github.com/equodev/equo-ide/pull/193 - .filter(f -> !f.getName().contains("groovy-test")) - .toList(); - return JarState.preserveOrder(classpath); + return JarState.preserveOrder(p2Provisioner.provisionP2Dependencies(modelWrapper, mavenProvisioner, cacheDirectory)); }), stepProperties.build()); return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); } + private @Nullable List readEmbeddedLockfileDependencies(String version) { + String lockfileResourcePath = lockfileResourcePath(version); + if (lockfileResourcePath == null) { + return null; + } + if (!lockfileResourcePath.startsWith("/")) { + throw new IllegalArgumentException("Lockfile resource path must start with '/': " + lockfileResourcePath); + } + InputStream lockfile = EquoBasedStepBuilder.class.getResourceAsStream(lockfileResourcePath); + if (lockfile == null) { + // No lockfile embedded for this version โ€” fall back to P2 provisioning + return null; + } + String allLines; + try (lockfile) { + allLines = new String(lockfile.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw Errors.asRuntime(e); + } + var dependencies = new ArrayList(); + for (String line : allLines.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("#")) { + dependencies.add(trimmed); + } + } + if (dependencies.isEmpty()) { + throw new IllegalArgumentException("No dependencies defined in lockfile " + lockfileResourcePath); + } + return dependencies; + } + + /** + * Returns the classpath resource path of an embedded lockfile for the given formatter version. + *

+ * Defaults to the same layout {@link EclipseBasedStepBuilder} uses -- {@code v.lockfile} + * inside a directory named after the formatter -- so a step picks up lockfile support simply by + * having its lockfiles checked in. Versions with no embedded lockfile fall back to P2 + * provisioning, which is what keeps Eclipse releases newer than Spotless usable. + *

+ * Overriding implementations should return an absolute classpath resource path (starting with + * {@code /}) that is compatible with {@link Class#getResourceAsStream(String)}. + * + * @return absolute classpath resource path of the embedded lockfile, or {@code null} to always + * use P2 provisioning + */ + protected @Nullable String lockfileResourcePath(String version) { + return "/" + EclipseBasedStepBuilder.ECLIPSE_FORMATTER_RESOURCES + "/" + formatterName.replace(' ', '_') + "/v" + version + ".lockfile"; + } + private P2Model createModelWithMirrors() { P2Model model = model(formatterVersion); if (p2Mirrors.isEmpty()) { diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java index 3ce9486476..21817ff275 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Provisioner.java @@ -50,28 +50,42 @@ List provisionP2Dependencies( Provisioner mavenProvisioner, @Nullable File cacheDirectory) throws IOException; - /** Creates a non-caching P2Provisioner for simple use cases. */ + /** + * Creates a non-caching P2Provisioner for simple use cases. + *

+ * All queries are serialized on {@code P2Provisioner.class}. Gradle may fingerprint + * many Spotless tasks in parallel; each fingerprint serializes the equality + * {@code ConfigurationCacheHackList}, which eagerly resolves Eclipse/P2 jars. + * Concurrent Solstice queries race on the on-disk cache and fail with + * {@code Failed to provision P2 dependencies}, reported by Gradle as + * "ConfigurationCacheHackList cannot be serialized" + * (#3004, + * #2331). + */ static P2Provisioner createDefault() { return (modelWrapper, mavenProvisioner, cacheDirectory) -> { - try { - if (cacheDirectory != null) { - CacheLocations.override_p2data = cacheDirectory; - } - P2Model model = modelWrapper.unwrap(); - P2QueryResult query = model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); - var classpath = new ArrayList(); - var mavenDeps = new ArrayList(); - mavenDeps.add("dev.equo.ide:solstice:1.8.1"); - mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1"); - mavenDeps.addAll(query.getJarsOnMavenCentral()); - classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps)); - classpath.addAll(query.getJarsNotOnMavenCentral()); - for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) { - classpath.add(nested.getValue()); + // Serialize all P2 queries in this JVM โ€” Solstice's cache is not concurrent-safe. + synchronized (P2Provisioner.class) { + try { + if (cacheDirectory != null) { + CacheLocations.override_p2data = cacheDirectory; + } + P2Model model = modelWrapper.unwrap(); + P2QueryResult query = model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); + var classpath = new ArrayList(); + var mavenDeps = new ArrayList(); + mavenDeps.add("dev.equo.ide:solstice:1.8.2"); + mavenDeps.add("com.diffplug.durian:durian-swt.os:4.3.1"); + mavenDeps.addAll(query.getJarsOnMavenCentral()); + classpath.addAll(mavenProvisioner.provisionWithTransitives(false, mavenDeps)); + classpath.addAll(query.getJarsNotOnMavenCentral()); + for (var nested : NestedJars.inFiles(query.getJarsNotOnMavenCentral()).extractAllNestedJars()) { + classpath.add(nested.getValue()); + } + return classpath; + } catch (Exception e) { + throw new IOException("Failed to provision P2 dependencies", e); } - return classpath; - } catch (Exception e) { - throw new IOException("Failed to provision P2 dependencies", e); } }; } diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java index 55668eeb21..9d94cc1b73 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java @@ -34,7 +34,7 @@ public final class GrEclipseFormatterStep { private GrEclipseFormatterStep() {} private static final String NAME = "eclipse groovy formatter"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.36").add(21, "4.39"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.36").add(21, "4.41"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); @@ -70,6 +70,20 @@ protected P2Model model(String version) { "org.eclipse.jdt.groovy.core", "org.codehaus.groovy")); model.addFilterAndValidate("no-debug", filter -> filter.exclude("org.eclipse.jdt.debug")); + + model.addFilterAndValidate("no-unused", filter -> { + filter.exclude("org.eclipse.equinox.supplement"); + filter.exclude("org.eclipse.jdt.core.manipulation"); + filter.exclude("org.eclipse.jdt.ui"); + + filter.excludePrefix("com.sun.jna"); + filter.excludePrefix("org.eclipse.equinox.p2"); + filter.excludePrefix("org.eclipse.ltk"); + filter.excludePrefix("org.eclipse.team"); + filter.excludePrefix("org.eclipse.swt"); + filter.excludePrefix("org.eclipse.ui"); + }); + // work around https://github.com/groovy/groovy-eclipse/issues/1617 model.useMavenCentral = false; return model; diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java index d140d03042..2d50bea4b3 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java @@ -35,7 +35,7 @@ public final class EclipseJdtFormatterStep { private EclipseJdtFormatterStep() {} private static final String NAME = "eclipse jdt formatter"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.39"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "4.41"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.lockfile new file mode 100644 index 0000000000..2cbfe35e0f --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.10 +org.eclipse.jdt:org.eclipse.jdt.core:3.16.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.200 +org.eclipse.platform:org.eclipse.core.expressions:3.6.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.200 +org.eclipse.platform:org.eclipse.core.jobs:3.10.200 +org.eclipse.platform:org.eclipse.core.resources:3.13.200 +org.eclipse.platform:org.eclipse.core.runtime:3.15.100 +org.eclipse.platform:org.eclipse.equinox.app:1.4.0 +org.eclipse.platform:org.eclipse.equinox.common:3.10.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.100 +org.eclipse.platform:org.eclipse.osgi:3.13.200 +org.eclipse.platform:org.eclipse.text:3.8.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile new file mode 100644 index 0000000000..47a5454c25 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.11 +org.eclipse.jdt:org.eclipse.jdt.core:3.17.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.300 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.300 +org.eclipse.platform:org.eclipse.core.expressions:3.6.300 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.300 +org.eclipse.platform:org.eclipse.core.jobs:3.10.300 +org.eclipse.platform:org.eclipse.core.resources:3.13.300 +org.eclipse.platform:org.eclipse.core.runtime:3.15.200 +org.eclipse.platform:org.eclipse.equinox.app:1.4.100 +org.eclipse.platform:org.eclipse.equinox.common:3.10.300 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.300 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.300 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.200 +org.eclipse.platform:org.eclipse.osgi:3.13.300 +org.eclipse.platform:org.eclipse.text:3.8.100 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.lockfile new file mode 100644 index 0000000000..fae8e59670 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.12 +org.eclipse.jdt:org.eclipse.jdt.core:3.18.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.400 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.300 +org.eclipse.platform:org.eclipse.core.expressions:3.6.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.400 +org.eclipse.platform:org.eclipse.core.jobs:3.10.400 +org.eclipse.platform:org.eclipse.core.resources:3.13.400 +org.eclipse.platform:org.eclipse.core.runtime:3.15.300 +org.eclipse.platform:org.eclipse.equinox.app:1.4.200 +org.eclipse.platform:org.eclipse.equinox.common:3.10.400 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.400 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.400 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.300 +org.eclipse.platform:org.eclipse.osgi:3.14.0 +org.eclipse.platform:org.eclipse.text:3.8.200 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.13.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.13.lockfile new file mode 100644 index 0000000000..1c6f9d9b62 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.13.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.13 +org.eclipse.jdt:org.eclipse.jdt.core:3.19.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.400 +org.eclipse.platform:org.eclipse.core.expressions:3.6.500 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.500 +org.eclipse.platform:org.eclipse.core.jobs:3.10.500 +org.eclipse.platform:org.eclipse.core.resources:3.13.500 +org.eclipse.platform:org.eclipse.core.runtime:3.16.0 +org.eclipse.platform:org.eclipse.equinox.app:1.4.300 +org.eclipse.platform:org.eclipse.equinox.common:3.10.500 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.500 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.500 +org.eclipse.platform:org.eclipse.equinox.supplement:1.9.0 +org.eclipse.platform:org.eclipse.osgi:3.15.0 +org.eclipse.platform:org.eclipse.text:3.9.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.14.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.14.lockfile new file mode 100644 index 0000000000..7c237413de --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.14.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.14 +org.eclipse.jdt:org.eclipse.jdt.core:3.20.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.600 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.500 +org.eclipse.platform:org.eclipse.core.expressions:3.6.600 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.600 +org.eclipse.platform:org.eclipse.core.jobs:3.10.600 +org.eclipse.platform:org.eclipse.core.resources:3.13.600 +org.eclipse.platform:org.eclipse.core.runtime:3.17.0 +org.eclipse.platform:org.eclipse.equinox.app:1.4.300 +org.eclipse.platform:org.eclipse.equinox.common:3.10.600 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.600 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.9.100 +org.eclipse.platform:org.eclipse.osgi:3.15.100 +org.eclipse.platform:org.eclipse.text:3.10.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.15.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.15.lockfile new file mode 100644 index 0000000000..4ce8634ae9 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.15.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.15 +org.eclipse.jdt:org.eclipse.jdt.core:3.21.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.700 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.600 +org.eclipse.platform:org.eclipse.core.expressions:3.6.700 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.700 +org.eclipse.platform:org.eclipse.core.jobs:3.10.700 +org.eclipse.platform:org.eclipse.core.resources:3.13.700 +org.eclipse.platform:org.eclipse.core.runtime:3.17.100 +org.eclipse.platform:org.eclipse.equinox.app:1.4.400 +org.eclipse.platform:org.eclipse.equinox.common:3.11.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.700 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.700 +org.eclipse.platform:org.eclipse.equinox.supplement:1.9.200 +org.eclipse.platform:org.eclipse.osgi:3.15.200 +org.eclipse.platform:org.eclipse.text:3.10.100 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.16.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.16.lockfile new file mode 100644 index 0000000000..c2673bda4a --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.16.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.16 +org.eclipse.jdt:org.eclipse.jdt.core:3.22.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.700 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.700 +org.eclipse.platform:org.eclipse.core.expressions:3.6.800 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.700 +org.eclipse.platform:org.eclipse.core.jobs:3.10.800 +org.eclipse.platform:org.eclipse.core.resources:3.13.700 +org.eclipse.platform:org.eclipse.core.runtime:3.18.0 +org.eclipse.platform:org.eclipse.equinox.app:1.4.500 +org.eclipse.platform:org.eclipse.equinox.common:3.12.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.8.0 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.800 +org.eclipse.platform:org.eclipse.equinox.supplement:1.9.300 +org.eclipse.platform:org.eclipse.osgi:3.15.300 +org.eclipse.platform:org.eclipse.text:3.10.200 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.17.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.17.lockfile new file mode 100644 index 0000000000..5de970283c --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.17.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.17 +org.eclipse.jdt:org.eclipse.jdt.core:3.23.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.700 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.800 +org.eclipse.platform:org.eclipse.core.expressions:3.7.0 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.700 +org.eclipse.platform:org.eclipse.core.jobs:3.10.800 +org.eclipse.platform:org.eclipse.core.resources:3.13.800 +org.eclipse.platform:org.eclipse.core.runtime:3.19.0 +org.eclipse.platform:org.eclipse.equinox.app:1.5.0 +org.eclipse.platform:org.eclipse.equinox.common:3.13.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.8.0 +org.eclipse.platform:org.eclipse.equinox.registry:3.9.0 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.0 +org.eclipse.platform:org.eclipse.osgi:3.16.0 +org.eclipse.platform:org.eclipse.text:3.10.300 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.18.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.18.lockfile new file mode 100644 index 0000000000..af354c5d5e --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.18.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.18 +org.eclipse.jdt:org.eclipse.jdt.core:3.24.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.800 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.800 +org.eclipse.platform:org.eclipse.core.expressions:3.7.0 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.700 +org.eclipse.platform:org.eclipse.core.jobs:3.10.1000 +org.eclipse.platform:org.eclipse.core.resources:3.13.900 +org.eclipse.platform:org.eclipse.core.runtime:3.20.0 +org.eclipse.platform:org.eclipse.equinox.app:1.5.0 +org.eclipse.platform:org.eclipse.equinox.common:3.14.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.8.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.10.0 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.0 +org.eclipse.platform:org.eclipse.osgi:3.16.100 +org.eclipse.platform:org.eclipse.text:3.10.400 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.19.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.19.lockfile new file mode 100644 index 0000000000..d5bbcfa27f --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.19.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.19 +org.eclipse.jdt:org.eclipse.jdt.core:3.25.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.800 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.900 +org.eclipse.platform:org.eclipse.core.expressions:3.7.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.700 +org.eclipse.platform:org.eclipse.core.jobs:3.10.1100 +org.eclipse.platform:org.eclipse.core.resources:3.14.0 +org.eclipse.platform:org.eclipse.core.runtime:3.20.100 +org.eclipse.platform:org.eclipse.equinox.app:1.5.100 +org.eclipse.platform:org.eclipse.equinox.common:3.14.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.8.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.10.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.100 +org.eclipse.platform:org.eclipse.osgi:3.16.200 +org.eclipse.platform:org.eclipse.text:3.11.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.lockfile new file mode 100644 index 0000000000..445c9f007f --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.20 +org.eclipse.jdt:org.eclipse.jdt.core:3.26.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.0 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.1000 +org.eclipse.platform:org.eclipse.core.expressions:3.7.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.0 +org.eclipse.platform:org.eclipse.core.jobs:3.11.0 +org.eclipse.platform:org.eclipse.core.resources:3.15.0 +org.eclipse.platform:org.eclipse.core.runtime:3.22.0 +org.eclipse.platform:org.eclipse.equinox.app:1.5.100 +org.eclipse.platform:org.eclipse.equinox.common:3.15.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.8.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.10.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.100 +org.eclipse.platform:org.eclipse.osgi:3.16.300 +org.eclipse.platform:org.eclipse.text:3.12.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.21.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.21.lockfile new file mode 100644 index 0000000000..2a889feb18 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.21.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.21 +org.eclipse.jdt:org.eclipse.jdt.core:3.27.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.100 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.0 +org.eclipse.platform:org.eclipse.core.expressions:3.8.0 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.100 +org.eclipse.platform:org.eclipse.core.jobs:3.12.0 +org.eclipse.platform:org.eclipse.core.resources:3.15.100 +org.eclipse.platform:org.eclipse.core.runtime:3.23.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.0 +org.eclipse.platform:org.eclipse.equinox.common:3.15.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.9.0 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.0 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.200 +org.eclipse.platform:org.eclipse.osgi:3.17.0 +org.eclipse.platform:org.eclipse.text:3.12.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.22.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.22.lockfile new file mode 100644 index 0000000000..70fe49568f --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.22.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.22 +org.eclipse.jdt:org.eclipse.jdt.core:3.28.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.100 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.100 +org.eclipse.platform:org.eclipse.core.expressions:3.8.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.200 +org.eclipse.platform:org.eclipse.core.jobs:3.12.0 +org.eclipse.platform:org.eclipse.core.resources:3.16.0 +org.eclipse.platform:org.eclipse.core.runtime:3.24.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.100 +org.eclipse.platform:org.eclipse.equinox.common:3.15.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.9.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.300 +org.eclipse.platform:org.eclipse.osgi:3.17.100 +org.eclipse.platform:org.eclipse.text:3.12.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.23.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.23.lockfile new file mode 100644 index 0000000000..ea17095f44 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.23.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.23 +org.eclipse.jdt:org.eclipse.jdt.core:3.29.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.100 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.100 +org.eclipse.platform:org.eclipse.core.expressions:3.8.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.300 +org.eclipse.platform:org.eclipse.core.jobs:3.12.100 +org.eclipse.platform:org.eclipse.core.resources:3.16.100 +org.eclipse.platform:org.eclipse.core.runtime:3.24.100 +org.eclipse.platform:org.eclipse.equinox.app:1.6.100 +org.eclipse.platform:org.eclipse.equinox.common:3.16.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.9.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.400 +org.eclipse.platform:org.eclipse.osgi:3.17.200 +org.eclipse.platform:org.eclipse.text:3.12.0 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.24.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.24.lockfile new file mode 100644 index 0000000000..c8f527718d --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.24.lockfile @@ -0,0 +1,17 @@ +# Spotless formatter based on Eclipse-JDT 4.24 +org.eclipse.jdt:org.eclipse.jdt.core:3.30.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.100 +org.eclipse.platform:org.eclipse.core.expressions:3.8.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.400 +org.eclipse.platform:org.eclipse.core.jobs:3.13.0 +org.eclipse.platform:org.eclipse.core.resources:3.17.0 +org.eclipse.platform:org.eclipse.core.runtime:3.25.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.100 +org.eclipse.platform:org.eclipse.equinox.common:3.16.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.1 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.500 +org.eclipse.platform:org.eclipse.osgi:3.18.0 +org.eclipse.platform:org.eclipse.text:3.12.100 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile new file mode 100644 index 0000000000..c51599e651 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.25.lockfile @@ -0,0 +1,17 @@ +# Spotless formatter based on Eclipse-JDT 4.25 +org.eclipse.jdt:org.eclipse.jdt.core:3.31.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.200 +org.eclipse.platform:org.eclipse.core.expressions:3.8.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.500 +org.eclipse.platform:org.eclipse.core.jobs:3.13.100 +org.eclipse.platform:org.eclipse.core.resources:3.18.0 +org.eclipse.platform:org.eclipse.core.runtime:3.26.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.16.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.600 +org.eclipse.platform:org.eclipse.osgi:3.18.100 +org.eclipse.platform:org.eclipse.text:3.12.200 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile new file mode 100644 index 0000000000..b4c053796e --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.26.lockfile @@ -0,0 +1,17 @@ +# Spotless formatter based on Eclipse-JDT 4.26 +org.eclipse.jdt:org.eclipse.jdt.core:3.32.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.300 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.200 +org.eclipse.platform:org.eclipse.core.expressions:3.8.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.500 +org.eclipse.platform:org.eclipse.core.jobs:3.13.200 +org.eclipse.platform:org.eclipse.core.resources:3.18.100 +org.eclipse.platform:org.eclipse.core.runtime:3.26.100 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.17.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.600 +org.eclipse.platform:org.eclipse.osgi:3.18.200 +org.eclipse.platform:org.eclipse.text:3.12.300 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.27.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.27.lockfile new file mode 100644 index 0000000000..a50ad530ef --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.27.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.27 +org.eclipse.jdt:org.eclipse.jdt.core:3.33.0 +org.eclipse.jdt:ecj:3.33.0 +org.eclipse.platform:org.eclipse.core.commands:3.10.400 +org.eclipse.platform:org.eclipse.core.contenttype:3.8.200 +org.eclipse.platform:org.eclipse.core.expressions:3.8.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.9.500 +org.eclipse.platform:org.eclipse.core.jobs:3.13.300 +org.eclipse.platform:org.eclipse.core.resources:3.18.200 +org.eclipse.platform:org.eclipse.core.runtime:3.26.100 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.17.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.600 +org.eclipse.platform:org.eclipse.osgi:3.18.300 +org.eclipse.platform:org.eclipse.text:3.12.300 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.28.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.28.lockfile new file mode 100644 index 0000000000..19746c78b2 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.28.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.28 +org.eclipse.jdt:org.eclipse.jdt.core:3.34.0 +org.eclipse.jdt:ecj:3.34.0 +org.eclipse.platform:org.eclipse.core.commands:3.11.0 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.0 +org.eclipse.platform:org.eclipse.core.expressions:3.9.0 +org.eclipse.platform:org.eclipse.core.filesystem:1.10.0 +org.eclipse.platform:org.eclipse.core.jobs:3.14.0 +org.eclipse.platform:org.eclipse.core.resources:3.19.0 +org.eclipse.platform:org.eclipse.core.runtime:3.27.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.200 +org.eclipse.platform:org.eclipse.equinox.common:3.18.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.700 +org.eclipse.platform:org.eclipse.osgi:3.18.400 +org.eclipse.platform:org.eclipse.text:3.13.0 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.29.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.29.lockfile new file mode 100644 index 0000000000..66b277c6c9 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.29.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.29 +org.eclipse.jdt:org.eclipse.jdt.core:3.35.0 +org.eclipse.jdt:ecj:3.35.0 +org.eclipse.platform:org.eclipse.core.commands:3.11.100 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.100 +org.eclipse.platform:org.eclipse.core.expressions:3.9.100 +org.eclipse.platform:org.eclipse.core.filesystem:1.10.100 +org.eclipse.platform:org.eclipse.core.jobs:3.15.0 +org.eclipse.platform:org.eclipse.core.resources:3.19.100 +org.eclipse.platform:org.eclipse.core.runtime:3.29.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.300 +org.eclipse.platform:org.eclipse.equinox.common:3.18.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.300 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.300 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.700 +org.eclipse.platform:org.eclipse.osgi:3.18.500 +org.eclipse.platform:org.eclipse.text:3.13.100 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.30.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.30.lockfile new file mode 100644 index 0000000000..21a07f7fb9 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.30.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.30 +org.eclipse.jdt:org.eclipse.jdt.core:3.36.0 +org.eclipse.jdt:ecj:3.36.0 +org.eclipse.platform:org.eclipse.core.commands:3.11.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.200 +org.eclipse.platform:org.eclipse.core.expressions:3.9.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.10.200 +org.eclipse.platform:org.eclipse.core.jobs:3.15.100 +org.eclipse.platform:org.eclipse.core.resources:3.20.0 +org.eclipse.platform:org.eclipse.core.runtime:3.30.0 +org.eclipse.platform:org.eclipse.equinox.app:1.6.400 +org.eclipse.platform:org.eclipse.equinox.common:3.18.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.10.400 +org.eclipse.platform:org.eclipse.equinox.registry:3.11.400 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.700 +org.eclipse.platform:org.eclipse.osgi:3.18.600 +org.eclipse.platform:org.eclipse.text:3.13.100 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.31.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.31.lockfile new file mode 100644 index 0000000000..df8e7feb5f --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.31.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.31 +org.eclipse.jdt:org.eclipse.jdt.core:3.37.0 +org.eclipse.jdt:ecj:3.37.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.0 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.300 +org.eclipse.platform:org.eclipse.core.expressions:3.9.300 +org.eclipse.platform:org.eclipse.core.filesystem:1.10.300 +org.eclipse.platform:org.eclipse.core.jobs:3.15.200 +org.eclipse.platform:org.eclipse.core.resources:3.20.100 +org.eclipse.platform:org.eclipse.core.runtime:3.31.0 +org.eclipse.platform:org.eclipse.equinox.app:1.7.0 +org.eclipse.platform:org.eclipse.equinox.common:3.19.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.0 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.0 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.800 +org.eclipse.platform:org.eclipse.osgi:3.19.0 +org.eclipse.platform:org.eclipse.text:3.14.0 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.32.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.32.lockfile new file mode 100644 index 0000000000..155347ba8e --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.32.lockfile @@ -0,0 +1,18 @@ +# Spotless formatter based on Eclipse-JDT 4.32 +org.eclipse.jdt:org.eclipse.jdt.core:3.38.0 +org.eclipse.jdt:ecj:3.38.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.100 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.400 +org.eclipse.platform:org.eclipse.core.expressions:3.9.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.10.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.300 +org.eclipse.platform:org.eclipse.core.resources:3.20.200 +org.eclipse.platform:org.eclipse.core.runtime:3.31.100 +org.eclipse.platform:org.eclipse.equinox.app:1.7.100 +org.eclipse.platform:org.eclipse.equinox.common:3.19.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.10.900 +org.eclipse.platform:org.eclipse.osgi:3.20.0 +org.eclipse.platform:org.eclipse.text:3.14.100 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.33.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.33.lockfile new file mode 100644 index 0000000000..b62e8b4478 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.33.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.33 +org.eclipse.jdt:org.eclipse.jdt.core:3.39.0 +net.java.dev.jna:jna-platform:5.14.0 +org.eclipse.jdt:ecj:3.39.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.500 +org.eclipse.platform:org.eclipse.core.expressions:3.9.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.0 +org.eclipse.platform:org.eclipse.core.jobs:3.15.400 +org.eclipse.platform:org.eclipse.core.resources:3.21.0 +org.eclipse.platform:org.eclipse.core.runtime:3.31.100 +org.eclipse.platform:org.eclipse.equinox.app:1.7.200 +org.eclipse.platform:org.eclipse.equinox.common:3.19.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.11.0 +org.eclipse.platform:org.eclipse.osgi:3.21.0 +org.eclipse.platform:org.eclipse.text:3.14.100 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.34.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.34.lockfile new file mode 100644 index 0000000000..9e7d990a4a --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.34.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.34 +org.eclipse.jdt:org.eclipse.jdt.core:3.40.0 +net.java.dev.jna:jna-platform:5.15.0 +org.eclipse.jdt:ecj:3.40.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.600 +org.eclipse.platform:org.eclipse.core.expressions:3.9.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.100 +org.eclipse.platform:org.eclipse.core.jobs:3.15.400 +org.eclipse.platform:org.eclipse.core.resources:3.22.0 +org.eclipse.platform:org.eclipse.core.runtime:3.32.0 +org.eclipse.platform:org.eclipse.equinox.app:1.7.200 +org.eclipse.platform:org.eclipse.equinox.common:3.19.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.200 +org.eclipse.platform:org.eclipse.equinox.supplement:1.11.100 +org.eclipse.platform:org.eclipse.osgi:3.22.0 +org.eclipse.platform:org.eclipse.text:3.14.200 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.35.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.35.lockfile new file mode 100644 index 0000000000..16690074b4 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.35.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.35 +org.eclipse.jdt:org.eclipse.jdt.core:3.41.0 +net.java.dev.jna:jna-platform:5.16.0 +org.eclipse.jdt:ecj:3.41.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.300 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.600 +org.eclipse.platform:org.eclipse.core.expressions:3.9.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.100 +org.eclipse.platform:org.eclipse.core.jobs:3.15.500 +org.eclipse.platform:org.eclipse.core.resources:3.22.100 +org.eclipse.platform:org.eclipse.core.runtime:3.33.0 +org.eclipse.platform:org.eclipse.equinox.app:1.7.300 +org.eclipse.platform:org.eclipse.equinox.common:3.20.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.300 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.300 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.0 +org.eclipse.platform:org.eclipse.osgi:3.23.0 +org.eclipse.platform:org.eclipse.text:3.14.300 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.36.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.36.lockfile new file mode 100644 index 0000000000..3b55b19835 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.36.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.36 +org.eclipse.jdt:org.eclipse.jdt.core:3.42.0 +net.java.dev.jna:jna-platform:5.17.0 +org.eclipse.jdt:ecj:3.42.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.400 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.600 +org.eclipse.platform:org.eclipse.core.expressions:3.9.400 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.200 +org.eclipse.platform:org.eclipse.core.jobs:3.15.600 +org.eclipse.platform:org.eclipse.core.resources:3.22.200 +org.eclipse.platform:org.eclipse.core.runtime:3.33.100 +org.eclipse.platform:org.eclipse.equinox.app:1.7.400 +org.eclipse.platform:org.eclipse.equinox.common:3.20.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.11.400 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.400 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.100 +org.eclipse.platform:org.eclipse.osgi:3.23.100 +org.eclipse.platform:org.eclipse.text:3.14.300 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.37.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.37.lockfile new file mode 100644 index 0000000000..b91e896130 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.37.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.37 +org.eclipse.jdt:org.eclipse.jdt.core:3.43.0 +net.java.dev.jna:jna-platform:5.17.0 +org.eclipse.jdt:ecj:3.43.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.400 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.700 +org.eclipse.platform:org.eclipse.core.expressions:3.9.500 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.300 +org.eclipse.platform:org.eclipse.core.jobs:3.15.700 +org.eclipse.platform:org.eclipse.core.resources:3.23.0 +org.eclipse.platform:org.eclipse.core.runtime:3.34.0 +org.eclipse.platform:org.eclipse.equinox.app:1.7.500 +org.eclipse.platform:org.eclipse.equinox.common:3.20.200 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.0 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.500 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.100 +org.eclipse.platform:org.eclipse.osgi:3.23.200 +org.eclipse.platform:org.eclipse.text:3.14.400 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.38.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.38.lockfile new file mode 100644 index 0000000000..f02f3494bf --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.38.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.38 +org.eclipse.jdt:org.eclipse.jdt.core:3.44.0 +net.java.dev.jna:jna-platform:5.18.1 +org.eclipse.jdt:ecj:3.44.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.800 +org.eclipse.platform:org.eclipse.core.expressions:3.9.500 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.700 +org.eclipse.platform:org.eclipse.core.resources:3.23.100 +org.eclipse.platform:org.eclipse.core.runtime:3.34.100 +org.eclipse.platform:org.eclipse.equinox.app:1.7.500 +org.eclipse.platform:org.eclipse.equinox.common:3.20.300 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.100 +org.eclipse.platform:org.eclipse.osgi:3.24.0 +org.eclipse.platform:org.eclipse.text:3.14.500 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile new file mode 100644 index 0000000000..e86e95e3d3 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.39.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.39 +org.eclipse.jdt:org.eclipse.jdt.core:3.45.0 +net.java.dev.jna:jna-platform:5.18.1 +org.eclipse.jdt:ecj:3.45.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.800 +org.eclipse.platform:org.eclipse.core.expressions:3.9.500 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.700 +org.eclipse.platform:org.eclipse.core.resources:3.23.200 +org.eclipse.platform:org.eclipse.core.runtime:3.34.200 +org.eclipse.platform:org.eclipse.equinox.app:1.7.600 +org.eclipse.platform:org.eclipse.equinox.common:3.20.300 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.200 +org.eclipse.platform:org.eclipse.osgi:3.24.100 +org.eclipse.platform:org.eclipse.text:3.14.600 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile new file mode 100644 index 0000000000..f30d63dc8c --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.40.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.40 +org.eclipse.jdt:org.eclipse.jdt.core:3.46.0 +net.java.dev.jna:jna-platform:5.18.1 +org.eclipse.jdt:ecj:3.46.0 +org.eclipse.platform:org.eclipse.core.commands:3.12.500 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.800 +org.eclipse.platform:org.eclipse.core.expressions:3.9.600 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.400 +org.eclipse.platform:org.eclipse.core.jobs:3.15.800 +org.eclipse.platform:org.eclipse.core.resources:3.24.0 +org.eclipse.platform:org.eclipse.core.runtime:3.34.200 +org.eclipse.platform:org.eclipse.equinox.app:1.7.600 +org.eclipse.platform:org.eclipse.equinox.common:3.20.400 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.300 +org.eclipse.platform:org.eclipse.osgi:3.24.200 +org.eclipse.platform:org.eclipse.text:3.14.700 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.41.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.41.lockfile new file mode 100644 index 0000000000..6825f3562c --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.41.lockfile @@ -0,0 +1,19 @@ +# Spotless formatter based on Eclipse-JDT 4.41 +org.eclipse.jdt:org.eclipse.jdt.core:3.47.0 +net.java.dev.jna:jna-platform:5.19.1 +org.eclipse.jdt:ecj:3.46.100 +org.eclipse.platform:org.eclipse.core.commands:3.13.0 +org.eclipse.platform:org.eclipse.core.contenttype:3.9.900 +org.eclipse.platform:org.eclipse.core.expressions:3.9.700 +org.eclipse.platform:org.eclipse.core.filesystem:1.11.500 +org.eclipse.platform:org.eclipse.core.jobs:3.15.900 +org.eclipse.platform:org.eclipse.core.resources:3.24.100 +org.eclipse.platform:org.eclipse.core.runtime:3.35.0 +org.eclipse.platform:org.eclipse.equinox.app:1.7.600 +org.eclipse.platform:org.eclipse.equinox.common:3.21.0 +org.eclipse.platform:org.eclipse.equinox.preferences:3.12.100 +org.eclipse.platform:org.eclipse.equinox.registry:3.12.600 +org.eclipse.platform:org.eclipse.equinox.supplement:1.12.300 +org.eclipse.platform:org.eclipse.osgi:3.24.300 +org.eclipse.platform:org.eclipse.text:3.14.800 +org.osgi:org.osgi.service.prefs:1.1.2 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile new file mode 100644 index 0000000000..19ddfb2389 --- /dev/null +++ b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.lockfile @@ -0,0 +1,16 @@ +# Spotless formatter based on Eclipse-JDT 4.9 +org.eclipse.jdt:org.eclipse.jdt.core:3.15.0 +org.eclipse.platform:org.eclipse.core.commands:3.9.200 +org.eclipse.platform:org.eclipse.core.contenttype:3.7.100 +org.eclipse.platform:org.eclipse.core.expressions:3.6.200 +org.eclipse.platform:org.eclipse.core.filesystem:1.7.200 +org.eclipse.platform:org.eclipse.core.jobs:3.10.100 +org.eclipse.platform:org.eclipse.core.resources:3.13.100 +org.eclipse.platform:org.eclipse.core.runtime:3.15.0 +org.eclipse.platform:org.eclipse.equinox.app:1.3.600 +org.eclipse.platform:org.eclipse.equinox.common:3.10.100 +org.eclipse.platform:org.eclipse.equinox.preferences:3.7.200 +org.eclipse.platform:org.eclipse.equinox.registry:3.8.100 +org.eclipse.platform:org.eclipse.equinox.supplement:1.8.100 +org.eclipse.platform:org.eclipse.osgi:3.13.100 +org.eclipse.platform:org.eclipse.text:3.7.0 diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java new file mode 100644 index 0000000000..81b00d054d --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/EquoBasedStepBuilderLockfileTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.extra; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.StepHarness; + +import dev.equo.solstice.p2.P2Model; + +class EquoBasedStepBuilderLockfileTest { + + @Test + void missingEmbeddedLockfileFallsBackToP2() throws Exception { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + when(p2Provisioner.provisionP2Dependencies(any(), any(), any())).thenReturn(List.of()); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/missing.lockfile", p2Provisioner, mavenProvisioner); + StepHarness.forStep(builder.build()).test("class T {}", "class T {}"); + verify(p2Provisioner).provisionP2Dependencies(any(), any(), any()); + verifyNoInteractions(mavenProvisioner); + } + + @Test + void lockfilePathMustBeAbsolute() { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + EquoBasedStepBuilder builder = builderWithLockfilePath("com/diffplug/spotless/extra/empty.lockfile", + p2Provisioner, mavenProvisioner); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> StepHarness.forStep(builder.build()).test("class T {}", "class T {}")); + assertTrue(exception.getMessage().contains("must start with '/'")); + verifyNoInteractions(p2Provisioner, mavenProvisioner); + } + + @Test + void emptyEmbeddedLockfileThrows() { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/empty.lockfile", + p2Provisioner, mavenProvisioner); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> StepHarness.forStep(builder.build()).test("class T {}", "class T {}")); + assertTrue(exception.getMessage().contains("No dependencies defined in lockfile")); + verifyNoInteractions(p2Provisioner, mavenProvisioner); + } + + @Test + void embeddedLockfileUsesWithoutTransitivesAndAllCoordinates(@TempDir Path tempDir) throws Exception { + P2Provisioner p2Provisioner = mock(); + Provisioner mavenProvisioner = mock(); + File dummyJar = tempDir.resolve("spotless-lockfile-test.jar").toFile(); + assertTrue(dummyJar.createNewFile()); + when(mavenProvisioner.provisionWithTransitives(eq(false), anyCollection())).thenReturn(Set.of(dummyJar)); + EquoBasedStepBuilder builder = builderWithLockfilePath("/com/diffplug/spotless/extra/two-deps.lockfile", + p2Provisioner, mavenProvisioner); + + StepHarness.forStep(builder.build()).test("class T {}", "class T {}"); + + verify(mavenProvisioner).provisionWithTransitives(eq(false), assertArg((Collection coords) -> assertThat(coords) + .containsExactlyInAnyOrder( + "org.eclipse.jdt:org.eclipse.jdt.core:3.45.0", + "org.eclipse.jdt:ecj:3.45.0"))); + verifyNoInteractions(p2Provisioner); + } + + private static EquoBasedStepBuilder builderWithLockfilePath(String lockfilePath, P2Provisioner p2Provisioner, Provisioner mavenProvisioner) { + return new EquoBasedStepBuilder( + "lockfile test formatter", + mavenProvisioner, + p2Provisioner, + "4.41", + state -> input -> input, + ImmutableMap.builder()) { + @Override + protected P2Model model(String version) { + return new P2Model(); + } + + @Override + protected String lockfileResourcePath(String version) { + return lockfilePath; + } + }; + } + +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitRachetMergeBaseTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitRachetMergeBaseTest.java index f8592a895d..7645b9d4f6 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitRachetMergeBaseTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitRachetMergeBaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 DiffPlug + * Copyright 2020-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -331,6 +331,6 @@ private Git initRepo() throws IllegalStateException, GitAPIException, IOExceptio private void addAndCommit(Git git, String message) throws GitAPIException { git.add().addFilepattern(".").call(); - git.commit().setMessage(message).call(); + git.commit().setSign(false).setMessage(message).call(); } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java index ff8eeaeae7..1ad8066f98 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepTest.java @@ -15,19 +15,64 @@ */ package com.diffplug.spotless.extra.java; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.FieldSource; +import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.TestP2Provisioner; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Provisioner; import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; class EclipseJdtFormatterStepTest extends EquoResourceHarness { + + private static final String LOCKFILE_SUFFIX = ".lockfile"; + + /** + * Every supported version ships a lockfile, but actually formatting with one downloads that + * release's jars, so the round-trip tests below run against a representative sample rather than + * the whole range. The sample spans both of Eclipse's Maven dependency styles: + *

    + *
  • Range-based Maven POM dependencies: 4.9, 4.11, and 4.25
  • + *
  • Exact Maven POM dependencies: 4.26, 4.39, and the default version
  • + *
+ * The styles changed in + * eclipse-platform/eclipse.platform.releng#135, + * effective 4.26, which switched Maven dependency mapping from OSGi ranges to resolved concrete + * versions. It is a sampling boundary only -- it does not affect which versions ship a lockfile. + */ + private static final List SAMPLED_LOCKFILE_VERSIONS = List.of("4.9", "4.11", "4.25", "4.26", "4.39", EclipseJdtFormatterStep.defaultVersion()); + + /** + * The shipped lockfiles are exactly the supported range -- no gaps, no strays. Contiguity is + * what makes the P2 fallback predictable: which resolution path you get should not depend on + * which release you happened to pin. See {@link EclipseJdtLockfileMetadataTool}, and run + * {@code ./gradlew :lib-extra:updateEclipseJdtLockfiles} when the default version moves. + */ + @Test + void shippedLockfilesAreExactlyTheSupportedRange() throws IOException { + try (Stream files = Files.list(EclipseJdtLockfileMetadataTool.lockfileDir())) { + List shipped = files.map(path -> path.getFileName().toString()) + .filter(name -> name.endsWith(LOCKFILE_SUFFIX)) + .map(name -> name.substring(1, name.length() - LOCKFILE_SUFFIX.length())) + .toList(); + assertThat(shipped).containsExactlyInAnyOrderElementsOf(EclipseJdtLockfileMetadataTool.targetVersions()); + } + } + private static EquoBasedStepBuilder createBuilder() { return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), TestP2Provisioner.defaultProvisioner()); } @@ -37,15 +82,24 @@ public EclipseJdtFormatterStepTest() { } @ParameterizedTest - @MethodSource + @FieldSource("SAMPLED_LOCKFILE_VERSIONS") void formatWithVersion(String version) throws Exception { harnessFor(version).test("test.java", "package p; class C{}", "package p;\nclass C {\n}"); } - private static Stream formatWithVersion() { - return Stream.of("4.9", EclipseJdtFormatterStep.defaultVersion()); + @ParameterizedTest + @FieldSource("SAMPLED_LOCKFILE_VERSIONS") + void sampledLockfileVersionsDoNotUseP2(String version) { + P2Provisioner p2Provisioner = mock(); + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral(), p2Provisioner); + builder.setVersion(version); + StepHarnessWithFile.forStep(this, builder.build()).test( + "test.java", + "package p; class C{}", + "package p;\nclass C {\n}"); + verifyNoInteractions(p2Provisioner); } /** New format interface requires source file information to distinguish module-info from compilation unit */ diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java new file mode 100644 index 0000000000..bc4b4334bc --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtLockfileMetadataTool.java @@ -0,0 +1,291 @@ +/* + * Copyright 2016-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.extra.java; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import dev.equo.solstice.p2.P2ClientCache; +import dev.equo.solstice.p2.P2Model; +import dev.equo.solstice.p2.P2QueryCache; +import dev.equo.solstice.p2.P2QueryResult; + +/** + * Executable class for validating or updating embedded JDT lockfiles from Eclipse P2 metadata. + *

+ * Intended for manual execution via the following entrypoints: + *

+ * - {@link EclipseJdtLockfileMetadataTool.Verify#main(String[])}
+ * - {@link EclipseJdtLockfileMetadataTool.Update#main(String[])}
+ * 
+ */ +public class EclipseJdtLockfileMetadataTool { + + /** + * Every Eclipse release from {@value #OLDEST_MINOR}, inclusive, through the version Spotless + * defaults to gets an embedded lockfile, with no gaps. + *

+ * Contiguity is the point: a user on any supported version should get the same resolution path + * as a user on any other. Sampling versions instead would make the P2 fallback -- and the + * behavior differences that come with it -- depend on which release you happen to pin. + *

+ * Eclipse's switch from OSGi ranges to resolved concrete versions in its Maven metadata + * (eclipse-platform/eclipse.platform.releng#135, + * effective 4.26) deliberately does not factor in. Full explicit lockfiles can be + * produced for any target by taking Solstice's P2-resolved Maven coordinates directly, which + * are already fully version-resolved under either style. + */ + private static final int OLDEST_MINOR = 9; + + static List targetVersions() { + String defaultVersion = EclipseJdtFormatterStep.defaultVersion(); + int newestMinor = minorOf(defaultVersion); + if (newestMinor < OLDEST_MINOR) { + throw new IllegalStateException("Default version " + defaultVersion + " is older than 4." + OLDEST_MINOR); + } + List versions = new ArrayList<>(); + for (int minor = OLDEST_MINOR; minor <= newestMinor; minor++) { + versions.add("4." + minor); + } + return versions; + } + + private static int minorOf(String version) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected 4.x but got " + version); + } + return Integer.parseInt(version.substring(2).split("\\.")[0]); + } + + /** + * Verifies the JDT lockfiles at {@link #targetVersions()} against Eclipse P2 metadata. + *

+ * Exits non-zero if any lockfile is missing or out of date, so this can gate CI. + */ + public static class Verify { + + public static void main(String[] args) { + if (run(false) > 0) { + System.exit(1); + } + } + + } + + /** + * Updates the JDT lockfiles at {@link #targetVersions()} from Eclipse P2 metadata. + *

+ * Missing lockfiles will be created. + */ + public static class Update { + + public static void main(String[] args) { + if (run(true) > 0) { + System.exit(1); + } + } + + } + + private static final String JDT_ID = "org.eclipse.jdt.core"; + private static final String JDT_MAVEN_KEY = "org.eclipse.jdt:org.eclipse.jdt.core"; + private static final List ECLIPSE_UPDATE_BASE_URLS = List.of( + "https://download.eclipse.org/eclipse/updates/", + "https://archive.eclipse.org/eclipse/updates/"); + + /** Returns the number of failures. */ + private static int run(boolean update) { + Path lockfileDir = lockfileDir(); + Map lockfilesByVersion = targetLockfiles(lockfileDir, targetVersions()); + + int checked = 0; + int failures = 0; + for (Map.Entry entry : lockfilesByVersion.entrySet()) { + checked++; + String eclipseVersion = entry.getKey(); + Path lockfilePath = entry.getValue(); + try { + List allCoordinates = resolveTransitiveClosure(eclipseVersion); + String rootCoordinate = allCoordinates.get(0); + if (update) { + Files.writeString(lockfilePath, lockfileContent(eclipseVersion, allCoordinates), StandardCharsets.UTF_8); + System.out.println("WROTE v" + eclipseVersion + " -> " + allCoordinates.size() + " artifact(s), root: " + rootCoordinate); + } else { + if (!Files.exists(lockfilePath)) { + System.err.println("MISSING v" + eclipseVersion + " -> " + lockfilePath + " (expected root: " + rootCoordinate + ")"); + failures++; + continue; + } + List actualCoordinates = lockfileCoordinates(lockfilePath); + String mismatch = mismatchMessage(eclipseVersion, allCoordinates, actualCoordinates); + if (mismatch != null) { + System.err.println(mismatch); + failures++; + continue; + } + System.out.println("OK v" + eclipseVersion + " -> " + rootCoordinate + " (" + allCoordinates.size() + " coordinates)"); + } + } catch (Exception e) { + System.err.println("ERROR v" + eclipseVersion + " -> " + e.getMessage()); + failures++; + } + } + if (update) { + System.out.println("Updated " + checked + " lockfile(s) with " + failures + " issue(s)."); + } else { + System.out.println("Verified " + checked + " lockfile(s) with " + failures + " issue(s)."); + } + return failures; + } + + private static Map targetLockfiles(Path lockfileDir, List versions) { + Map targets = new LinkedHashMap<>(); + for (String version : versions) { + targets.put(version, lockfileDir.resolve("v" + version + ".lockfile")); + } + return targets; + } + + private static String lockfileContent(String eclipseVersion, List coordinates) { + String prefix = "# Spotless formatter based on Eclipse-JDT " + eclipseVersion + "\n"; + StringJoiner joiner = new StringJoiner("\n", prefix, "\n"); + coordinates.forEach(joiner::add); + return joiner.toString(); + } + + private static List lockfileCoordinates(Path lockfilePath) throws IOException { + try (var lines = Files.lines(lockfilePath, StandardCharsets.UTF_8)) { + List coordinates = lines.map(String::trim) + .filter(line -> !line.isEmpty()) + .filter(line -> !line.startsWith("#")) + .toList(); + if (coordinates.isEmpty()) { + throw new IllegalArgumentException("No dependency coordinate found in " + lockfilePath); + } + return coordinates; + } + } + + static String mismatchMessage(String eclipseVersion, List expectedCoordinates, List actualCoordinates) { + if (expectedCoordinates.equals(actualCoordinates)) { + return null; + } + String expectedRoot = expectedCoordinates.isEmpty() ? "" : expectedCoordinates.get(0); + String actualRoot = actualCoordinates.isEmpty() ? "" : actualCoordinates.get(0); + return "MISMATCH v" + eclipseVersion + " -> expected root: " + expectedRoot + " (" + expectedCoordinates.size() + + " coordinates), actual: " + actualRoot + " (" + actualCoordinates.size() + " coordinates)"; + } + + static Path lockfileDir() { + Path fromRepoRoot = Path.of("lib-extra", "src", "main", "resources", "com", "diffplug", "spotless", "extra", "eclipse_jdt_formatter"); + if (Files.isDirectory(fromRepoRoot)) { + return fromRepoRoot; + } + Path fromLibExtra = Path.of("src", "main", "resources", "com", "diffplug", "spotless", "extra", "eclipse_jdt_formatter"); + if (Files.isDirectory(fromLibExtra)) { + return fromLibExtra; + } + throw new IllegalStateException("Unable to locate eclipse_jdt_formatter resource directory"); + } + + /** + * Resolves full, explicit Maven coordinates from the P2 query result for the given Eclipse version. + *

+ * This supports both exact and range-based historical metadata because the P2 solver has already + * chosen concrete versions before exposing Maven coordinates. + */ + private static List resolveTransitiveClosure(String eclipseVersion) throws Exception { + P2QueryResult query = queryJdtFromP2(eclipseVersion); + + // "g:a" -> selected version + Map selected = new LinkedHashMap<>(); + for (String coordinate : query.getJarsOnMavenCentral()) { + String[] parts = coordinate.split(":"); + if (parts.length != 3) { + throw new IllegalStateException("Expected Maven coordinate g:a:v but got: " + coordinate); + } + String groupId = parts[0], artifactId = parts[1], version = parts[2]; + String key = groupId + ":" + artifactId; + String existing = selected.get(key); + if (existing == null || compareVersions(existing, version) < 0) { + selected.put(key, version); + } + } + String rootVersion = selected.remove(JDT_MAVEN_KEY); + if (rootVersion == null) { + throw new IllegalStateException("P2 result for Eclipse " + eclipseVersion + " did not contain " + JDT_MAVEN_KEY); + } + List result = new ArrayList<>(); + result.add(JDT_MAVEN_KEY + ":" + rootVersion); + selected.entrySet().stream() + .map(e -> e.getKey() + ":" + e.getValue()) + .sorted() + .forEach(result::add); + return result; + } + + private static P2QueryResult queryJdtFromP2(String eclipseVersion) throws Exception { + Exception lastError = null; + for (String baseUrl : ECLIPSE_UPDATE_BASE_URLS) { + try { + P2Model model = new P2Model(); + addPlatformRepo(model, eclipseVersion, baseUrl); + model.getInstall().add(JDT_ID); + return model.query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); + } catch (Exception e) { + lastError = e; + } + } + throw new IllegalStateException("Failed to query Eclipse " + eclipseVersion + " from known update sites", lastError); + } + + private static void addPlatformRepo(P2Model model, String version, String baseUrl) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected 4.x"); + } + model.addP2Repo(baseUrl + version + "/"); + } + + /** Compares two version strings numerically, segment by segment. Returns positive if v1 > v2. */ + private static int compareVersions(String v1, String v2) { + String[] p1 = v1.split("\\."); + String[] p2 = v2.split("\\."); + int len = Math.max(p1.length, p2.length); + for (int i = 0; i < len; i++) { + int n1 = i < p1.length ? parseVersionSegment(p1[i]) : 0; + int n2 = i < p2.length ? parseVersionSegment(p2[i]) : 0; + if (n1 != n2) + return Integer.compare(n1, n2); + } + return 0; + } + + private static int parseVersionSegment(String segment) { + try { + return Integer.parseInt(segment); + } catch (NumberFormatException e) { + return 0; + } + } + +} diff --git a/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile b/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile new file mode 100644 index 0000000000..d25c36fa9a --- /dev/null +++ b/lib-extra/src/test/resources/com/diffplug/spotless/extra/empty.lockfile @@ -0,0 +1 @@ +# intentionally empty lockfile used by EquoBasedStepBuilderLockfileTest diff --git a/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile b/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile new file mode 100644 index 0000000000..757276a5bb --- /dev/null +++ b/lib-extra/src/test/resources/com/diffplug/spotless/extra/two-deps.lockfile @@ -0,0 +1,3 @@ +# test lockfile +org.eclipse.jdt:org.eclipse.jdt.core:3.45.0 +org.eclipse.jdt:ecj:3.45.0 diff --git a/lib/build.gradle b/lib/build.gradle deleted file mode 100644 index b4175b2cba..0000000000 --- a/lib/build.gradle +++ /dev/null @@ -1,149 +0,0 @@ -import org.gradle.util.GradleVersion - -plugins { - id 'java-library' - id 'io.github.davidburstrom.version-compatibility' -} -ext.artifactId = project.artifactIdLib -version = rootProject.spotlessChangelog.versionNext -apply from: rootProject.file('gradle/java-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -def NEEDS_GLUE = [ - // (alphabetic order please) - 'cleanthat', - 'diktat', - 'flexmark', - 'gherkin', - 'googleJavaFormat', - 'gson', - 'jackson', - 'javaParser', - 'ktfmt', - 'ktlint', - 'palantirJavaFormat', - 'scalafmt', - 'sortPom', - 'tableTestFormatter', - 'zjsonPatch', -] - -for (glue in NEEDS_GLUE) { - sourceSets.register(glue) { - compileClasspath += sourceSets.main.output - runtimeClasspath += sourceSets.main.output - java {} - } -} - -versionCompatibility { - adapters { - // (alphabetic order please) - namespaces.register('Cleanthat') { - versions = [ - '2.1', - ] - targetSourceSetName = 'cleanthat' - } - namespaces.register('KtLint') { - // as discussed at https://github.com/diffplug/spotless/pull/1475 - // we will support no more than 2 breaking changes at a time = 3 incompatible versions - // we will try to drop down to only one version if a stable API can be maintained for a full year - versions = [ - '1.0.0', - ] - targetSourceSetName = 'ktlint' - } - namespaces.register('Diktat') { - versions = [ - '1.2.5', - '2.0.0', - ] - targetSourceSetName = 'diktat' - } - } -} - -tasks.named("check").configure { - dependsOn(tasks.named("testCompatibilityAdapters")) - dependsOn(tasks.named("testCompatibility")) -} - -dependencies { - compileOnly libs.slf4j.api - testCommonImplementation libs.slf4j.api - - // zero runtime reqs is a hard requirements for spotless-lib - // if you need a dep, put it in lib-extra - testCommonImplementation libs.junit.jupiter - testCommonImplementation libs.assertj.core - testCommonImplementation libs.durian.testlib - testCommonImplementation projects.testlib - testCommonRuntimeOnly "org.junit.platform:junit-platform-launcher" - - // GLUE CODE (alphabetic order please) - // cleanthat - cleanthatCompileOnly libs.cleanthat.java - compatCleanthat2Dot1CompileAndTestOnly libs.cleanthat.java - // diktat old supported version 1.x - compatDiktat1Dot2Dot5CompileOnly libs.diktat.rules - // diktat latest supported version 2.x - compatDiktat2Dot0Dot0CompileOnly libs.diktat.runner - // flexmark - flexmarkCompileOnly libs.flexmark.all - // gherkin - gherkinCompileOnly libs.gherkin.utils - gherkinCompileOnly libs.slf4j.api - // googleJavaFormat - googleJavaFormatCompileOnly libs.google.java.format - // gson - gsonCompileOnly libs.gson - // jackson - jacksonCompileOnly libs.jackson.databind - jacksonCompileOnly libs.jackson.dataformat.yaml - // javaParser - javaParserCompileOnly libs.javaparser.symbol.solver.core - // ktfmt - ktfmtCompileOnly libs.ktfmt - ktfmtCompileOnly("com.google.googlejavaformat:google-java-format") { - version { - strictly '1.7' // for JDK 8 compatibility - } - } - ktfmtCompileOnly libs.jsr305 - // ktlint latest supported version - compatKtLint1Dot0Dot0CompileAndTestOnly libs.ktlint.rule.engine - compatKtLint1Dot0Dot0CompileAndTestOnly libs.ktlint.ruleset.standard - compatKtLint1Dot0Dot0CompileAndTestOnly libs.slf4j.api - // palantirJavaFormat - palantirJavaFormatCompileOnly libs.palantir.java.format // this version needs to stay compilable against Java 8 for CI Job testNpm - // scalafmt - scalafmtCompileOnly libs.scalafmt.core - // sortPom - sortPomCompileOnly libs.sortpom.sorter - sortPomCompileOnly libs.slf4j.api - // tableTestFormatter - tableTestFormatterCompileOnly libs.tabletest.formatter.core - // zjsonPatch - zjsonPatchCompileOnly libs.zjsonpatch -} - -// we'll hold the core lib to a high standard -spotbugs { - // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). - reportLevel = com.github.spotbugs.snom.Confidence.valueOf('LOW') - excludeFilter = file("spotbugs-exclude.xml") -} - -apply from: rootProject.file('gradle/special-tests.gradle') -tasks.withType(Test).configureEach { - jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED" - - failOnNoDiscoveredTests = false -} - -jar { - for (glue in NEEDS_GLUE) { - from sourceSets.getByName(glue).output.classesDirs - } -} diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000000..6d8e5f9213 --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,208 @@ +import com.github.spotbugs.snom.Confidence + +plugins { + `java-library` + alias(libs.plugins.buildconfig) + alias(libs.plugins.version.compatibility) + id("spotless.java-setup") + id("spotless.java-publish") + id("spotless.special-tests") +} + +version = spotlessChangelog.versionNext + +val Provider.version: Provider + get() = map { it.version } + +buildConfig { + packageName("com.diffplug.spotless.java") + useJavaOutput { + defaultVisibility = true + } + buildConfigField("VERSION_CLEANTHAT", libs.cleanthat.java.version) + buildConfigField("VERSION_GJF", libs.google.java.format.version) + buildConfigField("VERSION_JAVAPARSER", libs.javaparser.symbol.solver.core.version) + buildConfigField("VERSION_PALANTIR_JAVA_FORMAT", libs.palantir.java.format.version) + buildConfigField("VERSION_PRINCE_OF_SPACE", libs.prince.of.space.core.version) + buildConfigField("VERSION_TABLETEST_FORMATTER", libs.tabletest.formatter.core.version) + + forClass("com.diffplug.spotless.antlr4", "Antlr4BuildConfig") { + buildConfigField("VERSION_ANTLR4", libs.antlr4.formatter.version) + } + forClass("com.diffplug.spotless.asciidoc", "AsciidocBuildConfig") { + buildConfigField("VERSION_ADOCFMT", libs.adocfmt.version) + } + forClass("com.diffplug.spotless.gherkin", "GherkinBuildConfig") { + buildConfigField("VERSION_GHERKIN_UTILS", libs.gherkin.utils.version) + buildConfigField("VERSION_GHERKIN", libs.cucumber.gherkin.version) + } + forClass("com.diffplug.spotless.json", "JsonBuildConfig") { + buildConfigField("VERSION_JACKSON", libs.jackson.databind.version) + buildConfigField("VERSION_JSON_SIMPLE", libs.json.simple.version) + buildConfigField("VERSION_ZJSONPATCH", libs.zjsonpatch.version) + } + forClass("com.diffplug.spotless.json.gson", "GsonBuildConfig") { + buildConfigField("VERSION_GSON", libs.gson.version) + } + forClass("com.diffplug.spotless.kotlin", "KotlinBuildConfig") { + buildConfigField("VERSION_DIKTAT", libs.diktat.runner.version) + buildConfigField("VERSION_KTFMT", libs.ktfmt.version) + buildConfigField("VERSION_KTLINT", libs.ktlint.rule.engine.version) + } + forClass("com.diffplug.spotless.markdown", "MarkdownBuildConfig") { + buildConfigField("VERSION_FLEXMARK", libs.flexmark.all.version) + buildConfigField("VERSION_FRESHMARK", libs.freshmark.version) + } + forClass("com.diffplug.spotless.pom", "PomBuildConfig") { + buildConfigField("VERSION_SORTPOM", libs.sortpom.sorter.version) + } + forClass("com.diffplug.spotless.rdf", "RdfBuildConfig") { + buildConfigField("VERSION_COOL_RDF_FORMATTER", libs.cool.rdf.formatter.version) + } + forClass("com.diffplug.spotless.scala", "ScalaBuildConfig") { + buildConfigField("VERSION_SCALAFMT", libs.scalafmt.core.version) + } + forClass("com.diffplug.spotless.yaml", "YamlBuildConfig") { + buildConfigField("VERSION_JACKSON", libs.jackson.dataformat.yaml.version) + } +} + +val needsGlue = + listOf( + // (alphabetic order please) + "cleanthat", + "diktat", + "flexmark", + "gherkin", + "googleJavaFormat", + "gson", + "jackson", + "javaParser", + "ktfmt", + "ktlint", + "lombokStubs", + "palantirJavaFormat", + "princeOfSpace", + "scalafmt", + "sortPom", + "tableTestFormatter", + "zjsonPatch", + ) + +val mainSourceSet = sourceSets.named("main") + +for (glue in needsGlue) { + sourceSets.register(glue) { + compileClasspath += mainSourceSet.get().output + runtimeClasspath += mainSourceSet.get().output + java {} + } +} + +versionCompatibility { + adapters { + // (alphabetic order please) + namespaces.register("Cleanthat") { + versions = listOf("2.1") + targetSourceSetName = "cleanthat" + } + namespaces.register("KtLint") { + // as discussed at https://github.com/diffplug/spotless/pull/1475 + // we will support no more than 2 breaking changes at a time = 3 incompatible versions + // we will try to drop down to only one version if a stable API can be maintained for a full + // year + versions = listOf("1.0.0") + targetSourceSetName = "ktlint" + } + namespaces.register("Diktat") { + versions = listOf("1.2.5", "2.0.0") + targetSourceSetName = "diktat" + } + } +} + +tasks.check { + dependsOn(tasks.named("testCompatibilityAdapters")) + dependsOn(tasks.named("testCompatibility")) +} + +dependencies { + compileOnly(libs.slf4j.api) + "testCommonImplementation"(libs.slf4j.api) + + // zero runtime reqs is a hard requirements for spotless-lib + // if you need a dep, put it in lib-extra + "testCommonImplementation"(libs.junit.jupiter) + "testCommonImplementation"(libs.assertj.core) + "testCommonImplementation"(libs.durian.testlib) + "testCommonImplementation"(projects.testlib) + "testCommonRuntimeOnly"("org.junit.platform:junit-platform-launcher") + + // GLUE CODE (alphabetic order please) + // cleanthat + "cleanthatCompileOnly"(libs.cleanthat.java) + "compatCleanthat2Dot1CompileAndTestOnly"(libs.cleanthat.java) + // diktat old supported version 1.x + "compatDiktat1Dot2Dot5CompileOnly"(pinnedLibs.diktat.rules) + // diktat latest supported version 2.x + "compatDiktat2Dot0Dot0CompileOnly"(libs.diktat.runner) + // flexmark + "flexmarkCompileOnly"(libs.flexmark.all) + // gherkin + "gherkinCompileOnly"(libs.gherkin.utils) + "gherkinCompileOnly"(libs.cucumber.gherkin) + "gherkinCompileOnly"(libs.slf4j.api) + // googleJavaFormat + "googleJavaFormatCompileOnly"(pinnedLibs.google.java.format) + // gson + "gsonCompileOnly"(libs.gson) + // jackson + "jacksonCompileOnly"(libs.jackson.databind) + "jacksonCompileOnly"(libs.jackson.dataformat.yaml) + // javaParser + "javaParserCompileOnly"(libs.javaparser.symbol.solver.core) + // ktfmt + "ktfmtCompileOnly"(libs.ktfmt) + "ktfmtCompileOnly"(pinnedLibs.google.java.format) // for JDK 8 compatibility + "ktfmtCompileOnly"(libs.jsr305) + // ktlint latest supported version + "compatKtLint1Dot0Dot0CompileAndTestOnly"(libs.ktlint.rule.engine) + "compatKtLint1Dot0Dot0CompileAndTestOnly"(libs.ktlint.ruleset.standard) + "compatKtLint1Dot0Dot0CompileAndTestOnly"(libs.slf4j.api) + // palantirJavaFormat + "palantirJavaFormatCompileOnly"( + pinnedLibs.palantir.java.format + ) // this version needs to stay compilable against Java 8 for CI Job testNpm + // princeOfSpace + "princeOfSpaceCompileOnly"(libs.prince.of.space.core) + // scalafmt + "scalafmtCompileOnly"(libs.scalafmt.core) + // sortPom + "sortPomCompileOnly"(libs.sortpom.sorter) + "sortPomCompileOnly"(libs.slf4j.api) + // tableTestFormatter + "tableTestFormatterCompileOnly"(libs.tabletest.formatter.core) + // zjsonPatch + "zjsonPatchCompileOnly"(libs.zjsonpatch) + // lombokStubs โ€“ needs ECJ types to match the exact method descriptors that lombok + // transplants into ECJ's ASTConverter, ASTNode, etc. + "lombokStubsCompileOnly"("org.eclipse.jdt:org.eclipse.jdt.core:3.47.0") +} + +// we'll hold the core lib to a high standard +spotbugs { + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = Confidence.LOW + excludeFilter = file("spotbugs-exclude.xml") +} + +tasks.withType().configureEach { + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + failOnNoDiscoveredTests = false +} + +tasks.jar { + for (glue in needsGlue) { + from(sourceSets.getByName(glue).output.classesDirs) + } +} diff --git a/lib/gradle.properties b/lib/gradle.properties new file mode 100644 index 0000000000..c04b6557c2 --- /dev/null +++ b/lib/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=spotless-lib +POM_NAME=spotless-lib diff --git a/lib/spotbugs-exclude.xml b/lib/spotbugs-exclude.xml index 1739443b83..807778a209 100644 --- a/lib/spotbugs-exclude.xml +++ b/lib/spotbugs-exclude.xml @@ -20,4 +20,9 @@ + + + + + diff --git a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java index b981386b19..c563b7a4a2 100644 --- a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ExpandWildcardsFormatterFunc.java @@ -37,8 +37,11 @@ import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.FieldAccessExpr; import com.github.javaparser.ast.expr.MarkerAnnotationExpr; import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; import com.github.javaparser.ast.type.ClassOrInterfaceType; @@ -47,6 +50,7 @@ import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.JavaSymbolSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; @@ -187,6 +191,65 @@ public void visit(final MethodCallExpr n, final Map> importMap) { + Optional value = resolveValue(n); + if (value.isPresent()) { + // static imports of fields and enum constants, e.g. `PI` or `SECONDS` + staticMemberName(value.get()).ifPresent(name -> matchTypeName(importMap, name, true)); + } else if (isQualifier(n)) { + // a type that is only used to qualify a member, e.g. `Collections.sort(list)` + resolveTypeName(n).ifPresent(name -> matchTypeName(importMap, name, false)); + } + super.visit(n, importMap); + } + + private static Optional resolveValue(NameExpr n) { + try { + return Optional.of(n.resolve()); + } catch (RuntimeException ex) { + // not a resolvable variable, parameter, field or enum constant; possibly the name of a type or package + return Optional.empty(); + } + } + + private static Optional staticMemberName(ResolvedValueDeclaration value) { + if (value.isField() && value.asField().isStatic()) { + return Optional.of(value.asField().declaringType().getQualifiedName() + "." + value.getName()); + } + if (value.isEnumConstant()) { + ResolvedType enumType = value.getType(); + if (enumType.isReferenceType()) { + return Optional.of(enumType.asReferenceType().getQualifiedName() + "." + value.getName()); + } + } + return Optional.empty(); + } + + private static boolean isQualifier(NameExpr n) { + Node parent = n.getParentNode().orElse(null); + if (parent instanceof MethodCallExpr call) { + return call.getScope().filter(scope -> scope == n).isPresent(); + } + if (parent instanceof FieldAccessExpr access) { + return access.getScope() == n; + } + if (parent instanceof MethodReferenceExpr reference) { + return reference.getScope() == n; + } + return false; + } + + private static Optional resolveTypeName(NameExpr n) { + try { + ResolvedType type = n.calculateResolvedType(); + return type.isReferenceType() ? Optional.of(type.asReferenceType().getQualifiedName()) : Optional.empty(); + } catch (RuntimeException ex) { + // e.g. the first segment of a fully qualified name, which needs no import + return Optional.empty(); + } + } + private static R wrapUnsolvedSymbolException(T node, Function func) { try { return func.apply(node); diff --git a/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java new file mode 100644 index 0000000000..2fe629bac0 --- /dev/null +++ b/lib/src/javaParser/java/com/diffplug/spotless/glue/javaparser/ShortenQualifiedTypesFormatterFunc.java @@ -0,0 +1,611 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.glue.javaparser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.Position; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.RecordDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.TypeExpr; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import com.diffplug.spotless.FormatterFunc; + +/** + * Shortens fully-qualified type references and adds the corresponding imports. + * + *

Goal: never introduce a compile error

+ * + *

Spotless runs without a full classpath or type-resolution, so the formatter + * cannot know every type visible to the compiler. The guiding principle is: + * "when in doubt, leave it alone." An un-shortened reference is merely + * verbose; a wrongly-shortened one breaks the build. + * + *

What we can check exactly

+ *
    + *
  • Type context (AST). JavaParser tells us exactly which tokens are + * {@code ClassOrInterfaceType} nodes, so we never touch strings, comments, + * or non-type expressions when shortening type references.
  • + *
  • Import collisions. If the simple name is already imported to a + * different FQN, we leave the reference qualified.
  • + *
  • Declared-type collisions. If the simple name matches a class, + * enum, or record declared in the same file, we leave it.
  • + *
  • Unknown inheritance. A superclass or superinterface that is not + * declared in this file can contribute member types. Those names are + * invisible without a classpath, so a type with such a supertype + * (including through a same-file supertype) is left qualified. An + * implicit {@code java.lang.Object} does not count.
  • + *
  • Unqualified-reference collisions. If the simple name is already + * used unqualified elsewhere (possibly resolving to a same-package type), + * adding an import could silently change what it resolves to.
  • + *
+ * + *

Expression-context heuristics

+ * + *

FQTs also appear in expression context โ€” static method calls + * ({@code java.lang.management.ManagementFactory.getPlatformMXBeans(โ€ฆ)}), + * static field / enum-constant access + * ({@code java.util.concurrent.TimeUnit.SECONDS}), and nested-type member + * access ({@code pkg.models.CustomTypeProperty.TypeEnum.STRING}). + * JavaParser sees these as {@code FieldAccessExpr}/{@code MethodCallExpr}, + * not type nodes, so we apply two heuristics to avoid false positives: + * + *

    + *
  1. Known-package check. If the candidate FQN's package already + * appears in the file's imports, own package declaration, or is + * {@code java.lang}, we trust it.
  2. + *
  3. Minimum-depth fallback. Otherwise we require at least two + * lowercase (package) segments before the first uppercase (type) segment. + * This filters out {@code variable.Field} patterns that look superficially + * like a FQT but are really field access on a local variable. + *
    In theory this could skip a legitimate single-segment package + * (e.g. {@code a.MyType.method()}) that has no import in the file yet, + * but single-segment packages are virtually non-existent in practice.
  4. + *
+ * + *

The parser gives us accurate type-context identification (no false positives + * from strings, comments, or non-type contexts). Text-level replacement preserves + * the original formatting exactly. + */ +public class ShortenQualifiedTypesFormatterFunc implements FormatterFunc { + + private final JavaParser parser = new JavaParser( + new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE)); + + @Override + public String apply(String rawUnix) throws Exception { + ParseResult parseResult = parser.parse(rawUnix); + if (!parseResult.isSuccessful() || parseResult.getResult().isEmpty()) { + return rawUnix; + } + CompilationUnit cu = parseResult.getResult().get(); + + // 1. Collect the package name + String packageName = cu.getPackageDeclaration() + .map(PackageDeclaration::getNameAsString) + .orElse(""); + + // 2. Collect existing non-static imports + Map existingImportsBySimple = new LinkedHashMap<>(); + Set existingImportFqns = new LinkedHashSet<>(); + for (ImportDeclaration imp : cu.getImports()) { + if (imp.isStatic() || imp.isAsterisk()) { + continue; + } + String fqn = imp.getNameAsString(); + existingImportFqns.add(fqn); + String simple = fqn.substring(fqn.lastIndexOf('.') + 1); + existingImportsBySimple.put(simple, fqn); + } + + // 3. Collect type names declared in this file (top-level + nested) + Set declaredTypeNames = new LinkedHashSet<>(); + cu.findAll(ClassOrInterfaceDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); + cu.findAll(EnumDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); + cu.findAll(RecordDeclaration.class).forEach(c -> declaredTypeNames.add(c.getNameAsString())); + + // 4. Collect unqualified type references, which may resolve to types in the same package + Set unqualifiedTypeNames = new LinkedHashSet<>(); + cu.findAll(ClassOrInterfaceType.class).stream() + .filter(type -> type.getScope().isEmpty()) + .forEach(type -> unqualifiedTypeNames.add(type.getNameAsString())); + + // 5. Build set of known packages from existing imports, own package, and java.lang + Set knownPackages = new LinkedHashSet<>(); + knownPackages.add("java.lang"); + if (!packageName.isEmpty()) { + knownPackages.add(packageName); + } + for (String fqn : existingImportFqns) { + int lastDot = fqn.lastIndexOf('.'); + if (lastDot > 0) { + knownPackages.add(fqn.substring(0, lastDot)); + } + } + + // 6. Walk the AST to find outermost fully-qualified type nodes + Map> simpleToFqns = new LinkedHashMap<>(); + List qualifiedRefs = new ArrayList<>(); + + cu.accept(new CollectQualifiedTypesVisitor(simpleToFqns, qualifiedRefs, knownPackages), null); + + if (qualifiedRefs.isEmpty()) { + return rawUnix; + } + + // 7. Determine which FQNs are safe to shorten + Set safeToShorten = new LinkedHashSet<>(); + for (Map.Entry> entry : simpleToFqns.entrySet()) { + String simple = entry.getKey(); + Set fqns = entry.getValue(); + if (fqns.size() > 1) { + continue; + } + String fqn = fqns.iterator().next(); + String existing = existingImportsBySimple.get(simple); + if (existing != null && !existing.equals(fqn)) { + continue; + } + // Skip if simple name clashes with a type declared or already referenced in this file + if (declaredTypeNames.contains(simple) + || (existing == null && unqualifiedTypeNames.contains(simple) && !isImplicitlyImported(fqn, packageName))) { + continue; + } + safeToShorten.add(fqn); + } + + if (safeToShorten.isEmpty()) { + return rawUnix; + } + + // 8. Convert line/column positions to string offsets and replace + // Build line-start offset table + int[] lineOffsets = buildLineOffsets(rawUnix); + Set> unknownInheritance = typesWithUnknownInheritance(cu); + + // Use a set keyed on start offset to deduplicate (JavaParser may visit the same node twice, + // e.g. for instanceof pattern variables) + Map removalsByStart = new LinkedHashMap<>(); + Set shortened = new LinkedHashSet<>(); + for (QualifiedTypeRef ref : qualifiedRefs) { + if (!safeToShorten.contains(ref.fqn) || insideUnknownInheritance(ref.node, unknownInheritance)) { + continue; + } + int scopeStartOffset = toOffset(lineOffsets, ref.scopeStart); + int nameStartOffset = toOffset(lineOffsets, ref.nameStart); + if (scopeStartOffset >= 0 && nameStartOffset > scopeStartOffset) { + removalsByStart.putIfAbsent(scopeStartOffset, new int[]{scopeStartOffset, nameStartOffset}); + shortened.add(ref.fqn); + } + } + List removals = new ArrayList<>(removalsByStart.values()); + + // Sort removals in reverse order so we can apply them without invalidating offsets + removals.sort(Comparator.comparingInt((int[] a) -> a[0]).reversed()); + + StringBuilder sb = new StringBuilder(rawUnix); + for (int[] removal : removals) { + sb.delete(removal[0], removal[1]); + } + + // 9. Add missing imports + Set newImports = new TreeSet<>(); + for (String fqn : shortened) { + if (isImplicitlyImported(fqn, packageName)) { + continue; + } + if (existingImportFqns.contains(fqn)) { + continue; + } + newImports.add(fqn); + } + + if (!newImports.isEmpty()) { + String result = sb.toString(); + int insertPos = findImportInsertPosition(result); + boolean afterExistingImport = IMPORT_LINE.matcher(result).find(); + + StringBuilder importBlock = new StringBuilder(); + if (!afterExistingImport) { + importBlock.append('\n'); + } + for (String fqn : newImports) { + importBlock.append("\nimport ").append(fqn).append(';'); + } + sb = new StringBuilder(result); + sb.insert(insertPos, importBlock); + } + + return sb.toString(); + } + + private record QualifiedTypeRef(String fqn, String simpleName, Position scopeStart, Position nameStart, Node node) {} + + /** Collects the outermost fully-qualified type nodes, along with the text range of the scope to remove. */ + private static final class CollectQualifiedTypesVisitor extends VoidVisitorAdapter { + private final Map> simpleToFqns; + private final List qualifiedRefs; + private final Set knownPackages; + + CollectQualifiedTypesVisitor(Map> simpleToFqns, List qualifiedRefs, + Set knownPackages) { + this.simpleToFqns = simpleToFqns; + this.qualifiedRefs = qualifiedRefs; + this.knownPackages = knownPackages; + } + + @Override + public void visit(ClassOrInterfaceType type, Void arg) { + super.visit(type, arg); + if (type.getScope().isEmpty()) { + return; + } + // Skip types that are themselves the scope of a parent type + if (type.getParentNode().isPresent() + && type.getParentNode().get() instanceof ClassOrInterfaceType parent + && parent.getScope().isPresent() + && parent.getScope().get() == type) { + return; + } + String rawName = buildRawName(type); + if (!startsWithPackage(rawName)) { + return; + } + String simple = type.getNameAsString(); + // A method-reference scope (`scope::method`) is parsed as a type even when it is really a + // field access on an expression (e.g. `localVar.field::get`). Apply the same known-package / + // minimum-depth heuristic used for the other expression scopes before trusting it as a + // fully-qualified type (issue #3133). + if (isMethodReferenceScope(type) && !isTrustedFullyQualifiedName(rawName, knownPackages)) { + return; + } + simpleToFqns.computeIfAbsent(simple, k -> new LinkedHashSet<>()).add(rawName); + + // Record the text range of the scope (to be removed) + ClassOrInterfaceType scope = type.getScope().get(); + if (scope.getBegin().isPresent() && type.getName().getBegin().isPresent()) { + Position scopeStart = scope.getBegin().get(); + Position nameStart = type.getName().getBegin().get(); + qualifiedRefs.add(new QualifiedTypeRef(rawName, simple, scopeStart, nameStart, type)); + } + } + + @Override + public void visit(MethodCallExpr expr, Void arg) { + super.visit(expr, arg); + expr.getScope().ifPresent(this::processExpressionScope); + } + + @Override + public void visit(FieldAccessExpr expr, Void arg) { + super.visit(expr, arg); + if (expr.getParentNode().isPresent()) { + Node parent = expr.getParentNode().get(); + if (parent instanceof FieldAccessExpr fa && fa.getScope() == expr) { + return; + } + if (parent instanceof MethodCallExpr mc && mc.getScope().isPresent() && mc.getScope().get() == expr) { + return; + } + } + processExpressionScope(expr); + } + + /** Extracts a fully-qualified type from an expression chain of FieldAccessExpr/NameExpr nodes. */ + private void processExpressionScope(Expression expr) { + List chain = new ArrayList<>(); + Expression current = expr; + while (current instanceof FieldAccessExpr fa) { + chain.add(0, fa); + current = fa.getScope(); + } + if (!(current instanceof NameExpr ne)) { + return; + } + String rootName = ne.getNameAsString(); + if (rootName.isEmpty() || !Character.isLowerCase(rootName.charAt(0))) { + return; + } + int typeIdx = -1; + for (int i = 0; i < chain.size(); i++) { + String name = chain.get(i).getNameAsString(); + if (!name.isEmpty() && Character.isUpperCase(name.charAt(0))) { + typeIdx = i; + break; + } + } + if (typeIdx < 0) { + return; + } + StringBuilder fqn = new StringBuilder(rootName); + for (int i = 0; i <= typeIdx; i++) { + fqn.append('.').append(chain.get(i).getNameAsString()); + } + String fqnStr = fqn.toString(); + if (!isTrustedFullyQualifiedName(fqnStr, knownPackages)) { + return; + } + String simple = chain.get(typeIdx).getNameAsString(); + FieldAccessExpr typeNode = chain.get(typeIdx); + Expression typeScope = typeNode.getScope(); + if (typeScope.getBegin().isPresent() && typeNode.getName().getBegin().isPresent()) { + simpleToFqns.computeIfAbsent(simple, k -> new LinkedHashSet<>()).add(fqnStr); + qualifiedRefs.add(new QualifiedTypeRef(fqnStr, simple, + typeScope.getBegin().get(), typeNode.getName().getBegin().get(), typeNode)); + } + } + } + + /** + * A supertype that is not in this file can declare member types. Those names + * are in scope in the subtype, and the formatter cannot see them. + */ + private static Set> typesWithUnknownInheritance(CompilationUnit cu) { + List> decls = new ArrayList<>(); + decls.addAll(cu.findAll(ClassOrInterfaceDeclaration.class)); + decls.addAll(cu.findAll(EnumDeclaration.class)); + decls.addAll(cu.findAll(RecordDeclaration.class)); + Map, Boolean> memo = new IdentityHashMap<>(); + Set> visiting = Collections.newSetFromMap(new IdentityHashMap<>()); + Set> unknown = Collections.newSetFromMap(new IdentityHashMap<>()); + for (TypeDeclaration decl : decls) { + if (hasUnknownInheritance(decl, decls, memo, visiting)) { + unknown.add(decl); + } + } + return unknown; + } + + private static boolean hasUnknownInheritance(TypeDeclaration decl, List> all, + Map, Boolean> memo, Set> visiting) { + Boolean cached = memo.get(decl); + if (cached != null) { + return cached; + } + if (!visiting.add(decl)) { + return true; + } + boolean unknown = false; + for (ClassOrInterfaceType supertype : supertypes(decl)) { + if (isJavaLangObject(supertype, all)) { + continue; + } + TypeDeclaration resolved = resolveInFile(supertype, all); + if (resolved == null || hasUnknownInheritance(resolved, all, memo, visiting)) { + unknown = true; + break; + } + } + visiting.remove(decl); + memo.put(decl, unknown); + return unknown; + } + + private static List supertypes(TypeDeclaration decl) { + List types = new ArrayList<>(); + if (decl instanceof ClassOrInterfaceDeclaration classOrInterface) { + types.addAll(classOrInterface.getExtendedTypes()); + types.addAll(classOrInterface.getImplementedTypes()); + } else if (decl instanceof EnumDeclaration enumDecl) { + types.addAll(enumDecl.getImplementedTypes()); + } else if (decl instanceof RecordDeclaration recordDecl) { + types.addAll(recordDecl.getImplementedTypes()); + } + return types; + } + + /** {@code java.lang.Object} has no member types. A bare {@code Object} is that type unless this file declares one. */ + private static boolean isJavaLangObject(ClassOrInterfaceType type, List> all) { + if (type.getScope().isPresent()) { + return "java.lang.Object".equals(buildRawName(type)); + } + if (!"Object".equals(type.getNameAsString())) { + return false; + } + for (TypeDeclaration decl : all) { + if ("Object".equals(decl.getNameAsString())) { + return false; + } + } + return true; + } + + private static TypeDeclaration resolveInFile(ClassOrInterfaceType type, List> all) { + if (type.getScope().isPresent()) { + if (startsWithPackage(buildRawName(type))) { + return null; + } + TypeDeclaration scope = resolveInFile(type.getScope().get(), all); + if (scope == null) { + return null; + } + for (var member : scope.getMembers()) { + if (member instanceof TypeDeclaration nested && nested.getNameAsString().equals(type.getNameAsString())) { + return nested; + } + } + return null; + } + TypeDeclaration match = null; + for (TypeDeclaration decl : all) { + if (!decl.getNameAsString().equals(type.getNameAsString())) { + continue; + } + if (match != null) { + return null; + } + match = decl; + } + return match; + } + + private static boolean insideUnknownInheritance(Node node, Set> unknown) { + Node current = node; + while (current != null) { + if (current instanceof TypeDeclaration decl && unknown.contains(decl)) { + return true; + } + current = current.getParentNode().orElse(null); + } + return false; + } + + /** + * True if {@code type} is the scope (qualifier) of a method reference, i.e. the + * {@code scope} in {@code scope::method}. JavaParser parses such a scope as a + * {@link ClassOrInterfaceType} even when it is really a field access on an + * expression (e.g. {@code localVar.field::get}), so callers must verify it is a + * real type before shortening it. + */ + private static boolean isMethodReferenceScope(ClassOrInterfaceType type) { + ClassOrInterfaceType outer = type; + while (outer.getParentNode().isPresent() + && outer.getParentNode().get() instanceof ClassOrInterfaceType parent + && parent.getScope().isPresent() + && parent.getScope().get() == outer) { + outer = parent; + } + if (outer.getParentNode().isEmpty() || !(outer.getParentNode().get() instanceof TypeExpr typeExpr)) { + return false; + } + return typeExpr.getParentNode().isPresent() + && typeExpr.getParentNode().get() instanceof MethodReferenceExpr methodRef + && methodRef.getScope() == typeExpr; + } + + /** + * Decides whether a dotted name is a trustworthy fully-qualified type + * reference, mirroring the expression-scope heuristic: the first upper-case + * segment is the type; everything before it is the package. We trust it when the + * package is already known (from imports, the file's own package, or + * {@code java.lang}) or when there are at least two package segments. This + * rejects {@code variable.Field} / {@code variable.field} shapes that only look + * like a fully-qualified type. + */ + private static boolean isTrustedFullyQualifiedName(String dottedName, Set knownPackages) { + String[] segments = dottedName.split("\\."); + int typeIdx = -1; + for (int i = 0; i < segments.length; i++) { + if (!segments[i].isEmpty() && Character.isUpperCase(segments[i].charAt(0))) { + typeIdx = i; + break; + } + } + if (typeIdx <= 0) { + return false; + } + StringBuilder pkg = new StringBuilder(); + for (int i = 0; i < typeIdx; i++) { + if (i > 0) { + pkg.append('.'); + } + pkg.append(segments[i]); + } + return knownPackages.contains(pkg.toString()) || typeIdx >= 2; + } + + private static String buildRawName(ClassOrInterfaceType type) { + StringBuilder sb = new StringBuilder(); + buildRawNameRecursive(type, sb); + return sb.toString(); + } + + private static void buildRawNameRecursive(ClassOrInterfaceType type, StringBuilder sb) { + if (type.getScope().isPresent()) { + buildRawNameRecursive(type.getScope().get(), sb); + sb.append('.'); + } + sb.append(type.getNameAsString()); + } + + private static boolean startsWithPackage(String rawName) { + return !rawName.isEmpty() && Character.isLowerCase(rawName.charAt(0)); + } + + private static boolean isImplicitlyImported(String fqn, String packageName) { + return fqn.startsWith("java.lang.") && fqn.indexOf('.', 10) == -1 + || !packageName.isEmpty() && fqn.startsWith(packageName + ".") + && fqn.indexOf('.', packageName.length() + 1) == -1; + } + + /** Builds an array where lineOffsets[line] is the char offset of the start of that line (1-indexed). */ + private static int[] buildLineOffsets(String text) { + List offsets = new ArrayList<>(); + offsets.add(0); // dummy for 0-index + offsets.add(0); // line 1 starts at offset 0 + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '\n') { + offsets.add(i + 1); + } + } + return offsets.stream().mapToInt(Integer::intValue).toArray(); + } + + /** Converts a JavaParser Position (1-indexed line/column) to a string offset. */ + private static int toOffset(int[] lineOffsets, Position pos) { + if (pos.line < 1 || pos.line >= lineOffsets.length) { + return -1; + } + return lineOffsets[pos.line] + pos.column - 1; // column is 1-indexed + } + + private static final Pattern IMPORT_LINE = Pattern.compile("^[ \\t]*import\\s+[\\w.]+\\s*;", Pattern.MULTILINE); + private static final Pattern PACKAGE_LINE = Pattern.compile("^\\s*package\\s+[\\w.]+\\s*;", Pattern.MULTILINE); + + /** Finds the best position to insert new import statements. */ + private static int findImportInsertPosition(String text) { + Matcher m = IMPORT_LINE.matcher(text); + int lastImportEnd = -1; + while (m.find()) { + lastImportEnd = m.end(); + } + if (lastImportEnd >= 0) { + return lastImportEnd; + } + Matcher pkg = PACKAGE_LINE.matcher(text); + if (pkg.find()) { + return pkg.end(); + } + return 0; + } +} diff --git a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java index b99af17015..32db66f126 100644 --- a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java @@ -15,11 +15,15 @@ */ package com.diffplug.spotless.glue.ktfmt; +import java.io.File; + import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.facebook.ktfmt.format.Formatter; -import com.facebook.ktfmt.format.FormattingOptions; +import org.jetbrains.kotlinx.ktfmt.format.FileType; +import org.jetbrains.kotlinx.ktfmt.format.Formatter; +import org.jetbrains.kotlinx.ktfmt.format.FormattingOptions; +import org.jetbrains.kotlinx.ktfmt.format.KotlinCode; import com.diffplug.spotless.FormatterFunc; @@ -50,15 +54,26 @@ public KtfmtFormatterFunc(@Nonnull KtfmtStyle style, @Nullable KtfmtFormattingOp @Nonnull @Override public String apply(@Nonnull String input) throws Exception { - return Formatter.format(createFormattingOptions(), input); + return format(input, FileType.REGULAR); + } + + @Nonnull + @Override + public String apply(@Nonnull String input, @Nonnull File file) throws Exception { + FileType fileType = file.getName().endsWith("." + FileType.SCRIPT.getExtension()) ? FileType.SCRIPT : FileType.REGULAR; + return format(input, fileType); + } + + private String format(String input, FileType fileType) throws Exception { + return Formatter.format(createFormattingOptions(), KotlinCode.Companion.from(input, fileType)); } private FormattingOptions createFormattingOptions() throws Exception { FormattingOptions formattingOptions = switch (style) { - case META -> Formatter.META_FORMAT; - case GOOGLE -> Formatter.GOOGLE_FORMAT; - case KOTLIN_LANG -> Formatter.KOTLINLANG_FORMAT; - default -> throw new IllegalStateException("Unknown formatting option " + style); + case META -> Formatter.META_FORMAT; + case GOOGLE -> Formatter.GOOGLE_FORMAT; + case KOTLIN_LANG -> Formatter.KOTLINLANG_FORMAT; + default -> throw new IllegalStateException("Unknown formatting option " + style); }; if (ktfmtFormattingOptions == null) { diff --git a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java index f9ca437eee..3816d88e87 100644 --- a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,16 @@ */ package com.diffplug.spotless.glue.ktfmt; -import com.facebook.ktfmt.format.TrailingCommaManagementStrategy; +import org.jetbrains.kotlinx.ktfmt.format.TrailingCommaManagementStrategy; public enum KtfmtTrailingCommaManagementStrategy { NONE, ONLY_ADD, COMPLETE; public TrailingCommaManagementStrategy toFormatterTrailingCommaManagementStrategy() { return switch (this) { - case NONE -> TrailingCommaManagementStrategy.NONE; - case ONLY_ADD -> TrailingCommaManagementStrategy.ONLY_ADD; - case COMPLETE -> TrailingCommaManagementStrategy.COMPLETE; + case NONE -> TrailingCommaManagementStrategy.NONE; + case ONLY_ADD -> TrailingCommaManagementStrategy.ONLY_ADD; + case COMPLETE -> TrailingCommaManagementStrategy.COMPLETE; }; } } diff --git a/lib/src/lombokStubs/java/lombok/core/FieldAugment.java b/lib/src/lombokStubs/java/lombok/core/FieldAugment.java new file mode 100644 index 0000000000..53f8c428f9 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/core/FieldAugment.java @@ -0,0 +1,96 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.core; + +/** + * Stub implementation of {@code lombok.core.FieldAugment} used only within the + * {@code FeatureClassLoader} isolation boundary. The real FieldAugment is + * loaded by the lombok java-agent and is never reachable from Spotless's + * feature class-loader; this stub satisfies the static references that ECJ's + * patched classes make when lombok is active as a JVM agent. + * + *

{@link #augment} returns a no-op instance rather than {@code null} so that + * callers such as {@code EcjAugments} can safely call {@code .get()}, + * {@code .set()}, etc. on the returned object without a {@link NullPointerException}. + */ +@SuppressWarnings("unused") +public abstract class FieldAugment { + + /** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */ + @SuppressWarnings("unchecked") + public static FieldAugment augment(Class type, Class fieldType, String name) { + return (FieldAugment) NoopFieldAugment.INSTANCE; + } + + /** Returns a non-null no-op augment so callers can safely invoke instance methods on it. */ + @SuppressWarnings("unchecked") + public static FieldAugment circularSafeAugment(Class type, Class fieldType, String name) { + return (FieldAugment) NoopFieldAugment.INSTANCE; + } + + public abstract F get(T object); + + public abstract void set(T object, F value); + + public abstract F getAndSet(T object, F value); + + public abstract F clear(T object); + + public abstract F compareAndClear(T object, F expected); + + public abstract F setIfAbsent(T object, F value); + + public abstract F compareAndSet(T object, F expected, F value); + + /** Singleton no-op implementation returned by {@link #augment} and {@link #circularSafeAugment}. */ + @SuppressWarnings("rawtypes") + private static final class NoopFieldAugment extends FieldAugment { + static final NoopFieldAugment INSTANCE = new NoopFieldAugment(); + + @Override + public Object get(Object object) { + return null; + } + + @Override + public void set(Object object, Object value) {} + + @Override + public Object getAndSet(Object object, Object value) { + return null; + } + + @Override + public Object clear(Object object) { + return null; + } + + @Override + public Object compareAndClear(Object object, Object expected) { + return null; + } + + @Override + public Object setIfAbsent(Object object, Object value) { + return null; + } + + @Override + public Object compareAndSet(Object object, Object expected, Object value) { + return null; + } + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/EcjAugments.java b/lib/src/lombokStubs/java/lombok/eclipse/EcjAugments.java new file mode 100644 index 0000000000..38116588e0 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/EcjAugments.java @@ -0,0 +1,51 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse; + +import lombok.core.FieldAugment; + +/** + * Stub implementation of {@code lombok.eclipse.EcjAugments} used only within + * the {@code FeatureClassLoader} isolation boundary. + * + *

Each field is initialised via {@link FieldAugment#augment} so that callers + * (such as ECJ's patched {@code ASTConverter}) can safely invoke instance + * methods like {@code .get()} and {@code .set()} on them without a + * {@link NullPointerException}. The augment instances are no-ops that always + * return {@code null}. + */ +@SuppressWarnings({"unused", "rawtypes"}) +public final class EcjAugments { + + private EcjAugments() { + // prevent instantiation + } + + public static final FieldAugment ASTNode_generatedBy = FieldAugment.augment(Object.class, Object.class, "$generatedBy"); + public static final FieldAugment ASTNode_handled = FieldAugment.augment(Object.class, boolean.class, "lombok$handled"); + public static final FieldAugment ASTNode_tokens = FieldAugment.augment(Object.class, Object.class, "lombok$tokens"); + public static final FieldAugment FieldDeclaration_booleanLazyGetter = FieldAugment.augment(Object.class, boolean.class, "lombok$booleanLazyGetter"); + public static final FieldAugment Annotation_applied = FieldAugment.augment(Object.class, boolean.class, "lombok$applied"); + public static final FieldAugment CompilationUnit_javadoc = FieldAugment.augment(Object.class, Object.class, "$javadoc"); + public static final FieldAugment CompilationUnitDeclaration_transformationState = FieldAugment.augment(Object.class, Object.class, "$transformationState"); + + /** Stub inner class mirroring {@code EcjAugments.EclipseAugments}. */ + public static final class EclipseAugments { + private EclipseAugments() {} + + public static final FieldAugment CompilationUnit_delegateMethods = FieldAugment.augment(Object.class, Object.class, "$delegateMethods"); + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java new file mode 100644 index 0000000000..2e88cb3e4d --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/EclipseLoaderPatcherTransplants.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.EclipseLoaderPatcherTransplants} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class EclipseLoaderPatcherTransplants { + + /** Stub โ€“ returns {@code false} so the normal class-loading path is used. */ + public static boolean overrideLoadDecide(ClassLoader classLoader, String name, boolean resolve) { + return false; + } + + /** Stub โ€“ returns {@code null} so the caller falls through to normal loading. */ + public static Class overrideLoadResult(ClassLoader classLoader, String name, boolean resolve) throws ClassNotFoundException { + return null; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java new file mode 100644 index 0000000000..81c2830c2d --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDelegatePortal.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchDelegatePortal} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class PatchDelegatePortal { + + /** Stub โ€“ always returns {@code false}. */ + public static boolean handleDelegateForType(Object classScope) { + return false; + } + + /** Stub โ€“ returns an empty array. */ + public static Object[] addGeneratedDelegateMethods(Object returnValue, Object javaElement) { + return new Object[0]; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java new file mode 100644 index 0000000000..eace19397b --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchDiagnostics.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchDiagnostics} used + * only within the {@code FeatureClassLoader} isolation boundary. + * + *

When lombok is active as a JVM agent it patches ECJ's + * {@code ASTNode.setSourceRange} to call + * {@code PatchDiagnostics.setSourceRangeCheck}. The real class lives in + * lombok's shadow class-loader and is never reachable from + * {@code FeatureClassLoader}. This stub satisfies the call site so that + * source-range setting proceeds without a {@link NoSuchMethodError}. + * Returning {@code false} means "no override" โ€” ECJ proceeds with its + * normal source-range assignment. + */ +@SuppressWarnings("unused") +public class PatchDiagnostics { + + /** Stub โ€“ always returns {@code false} so ECJ applies its normal source-range logic. */ + public static boolean setSourceRangeCheck(Object node, int start, int end) { + return false; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java new file mode 100644 index 0000000000..2256b76ba9 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchFixesShadowLoaded.java @@ -0,0 +1,49 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchFixesShadowLoaded} + * used only within the {@code FeatureClassLoader} isolation boundary. + * Methods return their input unchanged; post-compiler steps are no-ops. + */ +@SuppressWarnings("unused") +public class PatchFixesShadowLoaded { + + /** Stub โ€“ returns {@code origReturnValue} unchanged. */ + public static String addLombokNotesToEclipseAboutDialog(String origReturnValue, String key) { + return origReturnValue; + } + + /** Stub โ€“ returns {@code bytes} unchanged. */ + public static byte[] runPostCompiler(byte[] bytes, String fileName) { + return bytes; + } + + /** Stub โ€“ returns {@code out} unchanged. */ + public static OutputStream runPostCompiler(OutputStream out) throws IOException { + return out; + } + + /** Stub โ€“ returns {@code out} unchanged. */ + public static BufferedOutputStream runPostCompiler(BufferedOutputStream out, String path, String name) throws IOException { + return out; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java new file mode 100644 index 0000000000..2179e22abe --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchJavadoc.java @@ -0,0 +1,29 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchJavadoc} + * used only within the {@code FeatureClassLoader} isolation boundary. + */ +@SuppressWarnings("unused") +public class PatchJavadoc { + + /** Stub โ€“ returns {@code original} unchanged. */ + public static String getHTMLContentFromSource(Object instance, String original, Object member) { + return original; + } +} diff --git a/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java new file mode 100644 index 0000000000..c82d0b3749 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/eclipse/agent/PatchValEclipsePortal.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.eclipse.agent; + +/** + * Stub implementation of {@code lombok.eclipse.agent.PatchValEclipsePortal} + * used only within the {@code FeatureClassLoader} isolation boundary. + * All methods are no-ops. + */ +@SuppressWarnings("unused") +public class PatchValEclipsePortal { + + public static void copyInitializationOfForEachIterable(Object parser) {} + + public static void copyInitializationOfLocalDeclaration(Object parser) {} + + public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, Object out, Object in) {} + + public static void addFinalAndValAnnotationToSingleVariableDeclaration(Object converter, Object out, Object in) {} +} diff --git a/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java b/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java new file mode 100644 index 0000000000..00f63bb912 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/launch/PatchFixesHider.java @@ -0,0 +1,283 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.launch; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.internal.compiler.ast.ASTNode; + +/** + * Stub implementation of {@code lombok.launch.PatchFixesHider} used only + * within the {@code FeatureClassLoader} isolation boundary. + * + *

When lombok is active as a JVM agent it patches ECJ classes so that their + * methods call into inner classes of {@code PatchFixesHider}. The real class + * lives in lombok's shadow class-loader and is never reachable from + * {@code FeatureClassLoader}. This stub satisfies those call sites so that + * ECJ's patched initialisers can complete without a {@link NoClassDefFoundError} + * or {@link NoSuchMethodError}. + * + *

Method signatures must exactly match the transplanted descriptors that + * lombok injects into ECJ bytecode; hence several methods use concrete ECJ + * types rather than {@code Object}. + */ +@SuppressWarnings("unused") +final class PatchFixesHider { + + private PatchFixesHider() {} + + /** Stub for {@code PatchFixesHider.ModuleClassLoading}. */ + public static final class ModuleClassLoading { + private ModuleClassLoading() {} + + /** Stub โ€“ no-op. */ + public static void parserClinit() {} + } + + /** Stub for {@code PatchFixesHider.Transform}. */ + public static final class Transform { + private Transform() {} + + /** Stub โ€“ no-op. */ + public static void transform(Object parser, Object ast) throws IOException {} + + /** Stub โ€“ no-op. */ + public static void transform_swapped(Object ast, Object parser) throws IOException {} + } + + /** Stub for {@code PatchFixesHider.PatchFixes}. */ + public static final class PatchFixes { + private PatchFixes() {} + + /** + * Stub โ€“ always returns {@code false}. + * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/core/dom/ASTNode;)Z} + */ + public static boolean isGenerated(org.eclipse.jdt.core.dom.ASTNode node) { + return false; + } + + /** + * Stub โ€“ always returns {@code false}. + * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;)Z} + */ + public static boolean isGenerated(ASTNode node) { + return false; + } + + /** + * Stub โ€“ always returns {@code false}. + * Matches transplanted descriptor: {@code (Lorg/eclipse/jdt/core/IMember;)Z} + */ + public static boolean isGenerated(org.eclipse.jdt.core.IMember member) { + return false; + } + + /** + * Stub โ€“ always returns {@code false}. + * Matches transplanted descriptor for isBlockedVisitorAndGenerated. + */ + public static boolean isBlockedVisitorAndGenerated(org.eclipse.jdt.core.dom.ASTNode node, ASTVisitor visitor) { + return false; + } + + /** Stub โ€“ always returns {@code false}. */ + public static boolean returnFalse(Object object) { + return false; + } + + /** Stub โ€“ always returns {@code true}. */ + public static boolean returnTrue(Object object) { + return true; + } + + /** Stub โ€“ returns {@code sourceEnd} unchanged. */ + public static int getSourceEndFixed(int sourceEnd, ASTNode node) throws Exception { + return sourceEnd; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static int fixRetrieveStartingCatchPosition(int original, int start) { + return original == -1 ? start : original; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static int fixRetrieveIdentifierEndPosition(int original, int start, int end) { + if (original == -1) + return end; + if (original < start) + return end; + return original; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static int fixRetrieveEllipsisStartPosition(int original, int end) { + return original == -1 ? end : original; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static int fixRetrieveStartBlockPosition(int original, int start) { + return original == -1 ? start : original; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static int fixRetrieveRightBraceOrSemiColonPosition(int original, int end) { + return original == -1 ? end : original; + } + + /** Stub โ€“ returns 0-length array. */ + public static Object[] listRewriteHandleGeneratedMethods(Object rewriteEvent) { + return new Object[0]; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static String getRealNodeSource(String original, ASTNode node) { + return original; + } + + /** Stub โ€“ returns {@code original} unchanged. */ + public static String getRealNodeSource(String original, org.eclipse.jdt.core.dom.ASTNode node) throws Exception { + return original; + } + } + + /** Stub for {@code PatchFixesHider.ValPortal}. */ + public static final class ValPortal { + private ValPortal() {} + + /** Stub โ€“ no-op. */ + public static void copyInitializationOfForEachIterable(Object parser) {} + + /** Stub โ€“ no-op. */ + public static void copyInitializationOfLocalDeclaration(Object parser) {} + + /** Stub โ€“ no-op. */ + public static void addFinalAndValAnnotationToVariableDeclarationStatement(Object converter, Object out, Object in) {} + + /** Stub โ€“ no-op. */ + public static void addFinalAndValAnnotationToSingleVariableDeclaration(Object converter, Object out, Object in) {} + } + + /** Stub for {@code PatchFixesHider.Val}. */ + public static final class Val { + private Val() {} + + /** Stub โ€“ always returns {@code false}. */ + public static boolean handleValForLocalDeclaration(Object local, Object scope) { + return false; + } + + /** Stub โ€“ always returns {@code false}. */ + public static boolean handleValForForEach(Object forEach, Object scope) { + return false; + } + } + + /** Stub for {@code PatchFixesHider.ExtensionMethod}. */ + public static final class ExtensionMethod { + private ExtensionMethod() {} + + /** Stub โ€“ returns {@code resolvedType} unchanged. */ + public static Object resolveType(Object resolvedType, Object methodCall, Object scope) { + return resolvedType; + } + + /** Stub โ€“ no-op. */ + public static void errorNoMethodFor(Object problemReporter, Object messageSend, Object recType, Object params) {} + + /** Stub โ€“ no-op. */ + public static void invalidMethod(Object problemReporter, Object messageSend, Object method) {} + + /** Stub โ€“ no-op. */ + public static void invalidMethod(Object problemReporter, Object messageSend, Object method, Object scope) {} + + /** Stub โ€“ no-op. */ + public static void nonStaticAccessToStaticMethod(Object problemReporter, Object location, Object method, Object messageSend) {} + + /** Stub โ€“ returns {@code original} unchanged. */ + public static Object modifyMethodPattern(Object original) { + return original; + } + } + + /** Stub for {@code PatchFixesHider.Delegate}. */ + public static final class Delegate { + private Delegate() {} + + /** Stub โ€“ always returns {@code false}. */ + public static boolean handleDelegateForType(Object classScope) { + return false; + } + + /** Stub โ€“ returns an empty array. */ + public static Object[] addGeneratedDelegateMethods(Object returnValue, Object javaElement) { + return new Object[0]; + } + + /** Stub โ€“ always returns {@code false}. */ + public static boolean isDelegateSourceMethod(Object sourceMethod) { + return false; + } + + /** Stub โ€“ always returns {@code null}. */ + public static Object returnElementInfo(Object delegateSourceMethod) { + return null; + } + } + + /** Stub for {@code PatchFixesHider.Util}. */ + public static final class Util { + private Util() {} + } + + /** Stub for {@code PatchFixesHider.LombokDeps}. */ + public static final class LombokDeps { + private LombokDeps() {} + + /** Stub โ€“ returns {@code origReturnValue} unchanged. */ + public static String addLombokNotesToEclipseAboutDialog(String origReturnValue, String key) { + return origReturnValue; + } + + /** Stub โ€“ returns {@code bytes} unchanged. */ + public static byte[] runPostCompiler(byte[] bytes, String fileName) { + return bytes; + } + + /** Stub โ€“ returns {@code out} unchanged. */ + public static OutputStream runPostCompiler(OutputStream out) throws IOException { + return out; + } + + /** Stub โ€“ returns {@code out} unchanged. */ + public static BufferedOutputStream runPostCompiler(BufferedOutputStream out, String path, String name) throws IOException { + return out; + } + } + + /** Stub for {@code PatchFixesHider.Javadoc}. */ + public static final class Javadoc { + private Javadoc() {} + + /** Stub โ€“ returns {@code original} unchanged. */ + public static String getHTMLContentFromSource(String original, Object member) { + return original; + } + } +} diff --git a/lib/src/lombokStubs/java/lombok/patcher/Symbols.java b/lib/src/lombokStubs/java/lombok/patcher/Symbols.java new file mode 100644 index 0000000000..9c02431a15 --- /dev/null +++ b/lib/src/lombokStubs/java/lombok/patcher/Symbols.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package lombok.patcher; + +import java.util.Collections; +import java.util.List; + +/** + * Stub implementation of {@code lombok.patcher.Symbols} used only within the + * {@code FeatureClassLoader} isolation boundary. + * + *

When lombok is active as a JVM agent it patches ECJ's + * {@code DefaultCodeFormatter} (and related classes) to call methods on + * {@code lombok.patcher.Symbols}. That class lives inside the lombok jar + * under a shadow class-loader prefix ({@code SCL.lombok/}) and is never + * reachable as a normal class from {@code FeatureClassLoader}. This stub + * satisfies the reference so that ECJ's patched code can load without a + * {@link NoClassDefFoundError}. Every method is a safe no-op. + */ +@SuppressWarnings("unused") +public class Symbols { + + private Symbols() {} + + /** Stub โ€“ no-op. */ + public static void push(String symbol) {} + + /** Stub โ€“ no-op. */ + public static void pop() {} + + /** Stub โ€“ always returns {@code true} (no symbols active). */ + public static boolean isEmpty() { + return true; + } + + /** Stub โ€“ always returns {@code 0}. */ + public static int size() { + return 0; + } + + /** Stub โ€“ always returns {@code false}. */ + public static boolean hasSymbol(String symbol) { + return false; + } + + /** Stub โ€“ always returns {@code false}. */ + public static boolean hasTail(String symbol) { + return false; + } + + /** Stub โ€“ always returns an empty list. */ + public static List getCopy() { + return Collections.emptyList(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java b/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java index 7ae51024f2..4e025462e3 100644 --- a/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java +++ b/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,13 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; +import java.util.Set; + +import javax.annotation.Nullable; import com.diffplug.spotless.yaml.SerializeToByteArrayHack; @@ -60,11 +65,47 @@ public final class ConfigurationCacheHackList implements Serializable { private boolean optimizeForEquality; private ArrayList backingList = new ArrayList<>(); + /** + * The failure from the most recent serialization attempt, if any. Not part of the + * serialized form - it exists only so {@link #toString()} can report why serialization + * failed without re-evaluating any step state. + */ + @Nullable private transient volatile String serializationFailure; + private boolean shouldWeSerializeToByteArrayFirst() { return backingList.stream().anyMatch(SerializeToByteArrayHack.class::isInstance); } private void writeObject(ObjectOutputStream out) throws IOException { + try { + writeSteps(out); + } catch (IOException | RuntimeException e) { + // Gradle reports a fingerprinting failure as "value '' cannot be + // serialized" and discards the cause, so stash it where toString() can report + // it. Otherwise the actionable message (e.g. "P2 dependencies not predeclared") + // is lost and the user only sees "cannot be serialized". See #3004. + serializationFailure = describeFailure(e); + throw e; + } + } + + /** Walks the cause chain so nested messages survive into {@link #toString()}. */ + private static String describeFailure(Throwable e) { + StringBuilder causes = new StringBuilder(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (Throwable t = e; t != null && seen.add(t); t = t.getCause()) { + String message = t.getMessage(); + if (message != null && !message.isEmpty()) { + if (causes.length() > 0) { + causes.append(" > "); + } + causes.append(message); + } + } + return causes.length() == 0 ? e.getClass().getName() : causes.toString(); + } + + private void writeSteps(ObjectOutputStream out) throws IOException { boolean serializeToByteArrayFirst = shouldWeSerializeToByteArrayFirst(); out.writeBoolean(serializeToByteArrayFirst); out.writeBoolean(optimizeForEquality); @@ -150,4 +191,28 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(optimizeForEquality, backingList); } + + /** + * Must not call {@link #hashCode()} โ€” that fingerprints every step and may provision + * P2/Maven deps. Gradle includes this value in "cannot be serialized" messages, so a + * side-effecting {@code toString} re-triggers provisioning while the build is already + * failing (see #3004). + *

+ * Gradle builds that message only after serialization has already thrown, so any + * failure is reported from {@link #serializationFailure} rather than by re-evaluating + * the steps. That keeps actionable errors such as "P2 dependencies not predeclared" + * visible to the user. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getName()) + .append('@').append(Integer.toHexString(System.identityHashCode(this))) + .append("[optimizeForEquality=").append(optimizeForEquality) + .append(", size=").append(backingList.size()); + String failure = serializationFailure; + if (failure != null) { + builder.append(", failure=").append(failure); + } + return builder.append(']').toString(); + } } diff --git a/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java b/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java index 0eb5aa4d2e..7f9cc500b4 100644 --- a/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java +++ b/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package com.diffplug.spotless; import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.security.ProtectionDomain; import java.util.Objects; @@ -73,6 +75,34 @@ protected Class findClass(String name) throws ClassNotFoundException { } catch (IOException e) { throw new ClassNotFoundException(name, e); } + } else if (name.startsWith("lombok.")) { + // When lombok is active as a JVM agent it patches ECJ classes (Parser, + // ASTConverter, DefaultCodeFormatter, etc.) to reference lombok.* classes + // that live in lombok's shadow class-loader (SCL.lombok/) and are never + // reachable from this isolated FeatureClassLoader. + // + // Strategy: + // 1. Serve a hand-written stub if one is bundled in the lib jar + // (covers FieldAugment, EcjAugments, PatchFixesHider, Symbols, ...). + // 2. Otherwise synthesise a minimal empty public class on the fly so that + // ECJ's patched static initialisers can complete without + // NoClassDefFoundError. The synthesised class has no fields or methods + // and performs no work; it purely satisfies the JVM's class-loading. + String path = name.replace('.', '/') + ".class"; + URL url = findResource(path); + if (url != null) { + try { + return defineClass(name, urlToByteBuffer(url), (ProtectionDomain) null); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + } + // No bundled stub โ€“ synthesise a minimal empty class. + try { + return defineClass(name, ByteBuffer.wrap(synthesiseEmptyClass(name)), (ProtectionDomain) null); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } } else if (useBuildToolClassLoader(name)) { return buildToolClassLoader.loadClass(name); } else { @@ -99,6 +129,132 @@ public URL findResource(String name) { return buildToolClassLoader.getResource(name); } + /** + * Synthesises a minimal valid class file for a public class with the given + * binary name, no superclass fields or methods beyond the default constructor + * inherited from {@link Object}. The result satisfies the JVM class-loader + * contract so that references to the class can be resolved without a + * {@link NoClassDefFoundError}, even though the class does nothing. + * + *

The bytecode is hand-assembled following the Java Virtual Machine + * Specification ยง4 (class file format). It requires no external libraries. + */ + private static byte[] synthesiseEmptyClass(String binaryName) throws IOException { + // Internal name uses '/' as separator; e.g. "lombok/eclipse/agent/PatchDiagnostics" + String internalName = binaryName.replace('.', '/'); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(256); + DataOutputStream out = new DataOutputStream(baos); + + // ---- Constant pool ---- + // We need: + // #1 Class_info โ†’ #2 (this class) + // #2 Utf8 internalName + // #3 Class_info โ†’ #4 (superclass: java/lang/Object) + // #4 Utf8 "java/lang/Object" + // #5 Utf8 "Code" (attribute name used by ) + // #6 Utf8 "" + // #7 Utf8 "()V" + // #8 MethodRef โ†’ #3, #9 + // #9 NameAndType โ†’ #6, #7 + int constantPoolCount = 10; // indices 1..9 + + // magic + out.writeInt(0xCAFEBABE); + // minor version, major version (52 = Java 8, safe floor for --release 17 output) + out.writeShort(0); + out.writeShort(52); + // constant pool count (number of entries + 1) + out.writeShort(constantPoolCount); + + // #1 CONSTANT_Class โ†’ #2 + out.writeByte(7); + out.writeShort(2); + // #2 CONSTANT_Utf8 internalName + out.writeByte(1); + writeUtf8(out, internalName); + // #3 CONSTANT_Class โ†’ #4 + out.writeByte(7); + out.writeShort(4); + // #4 CONSTANT_Utf8 "java/lang/Object" + out.writeByte(1); + writeUtf8(out, "java/lang/Object"); + // #5 CONSTANT_Utf8 "Code" + out.writeByte(1); + writeUtf8(out, "Code"); + // #6 CONSTANT_Utf8 "" + out.writeByte(1); + writeUtf8(out, ""); + // #7 CONSTANT_Utf8 "()V" + out.writeByte(1); + writeUtf8(out, "()V"); + // #8 CONSTANT_Methodref โ†’ class=#3, nameAndType=#9 + out.writeByte(10); + out.writeShort(3); + out.writeShort(9); + // #9 CONSTANT_NameAndType โ†’ name=#6, descriptor=#7 + out.writeByte(12); + out.writeShort(6); + out.writeShort(7); + + // ---- Class declaration ---- + // access flags: ACC_PUBLIC | ACC_SUPER + out.writeShort(0x0021); + // this_class = #1 + out.writeShort(1); + // super_class = #3 + out.writeShort(3); + // interfaces count + out.writeShort(0); + // fields count + out.writeShort(0); + // methods count: one method () + out.writeShort(1); + + // ---- ()V method ---- + // access: ACC_PUBLIC + out.writeShort(0x0001); + // name_index = #6 "" + out.writeShort(6); + // descriptor_index = #7 "()V" + out.writeShort(7); + // attributes count: 1 (Code) + out.writeShort(1); + + // Code attribute + // attribute_name_index = #5 "Code" + out.writeShort(5); + // attribute_length: max_stack(2) + max_locals(2) + code_length(4) + code(5) + exception_table_length(2) + attributes_count(2) = 17 + out.writeInt(17); + // max_stack + out.writeShort(1); + // max_locals + out.writeShort(1); + // code_length + out.writeInt(5); + // aload_0, invokespecial #8, return + out.writeByte(0x2A); // aload_0 + out.writeByte(0xB7); // invokespecial + out.writeShort(8); // โ†’ #8 Object. + out.writeByte(0xB1); // return + // exception_table_length + out.writeShort(0); + // attributes_count (for Code attribute) + out.writeShort(0); + + // ---- Class attributes ---- + out.writeShort(0); + + out.flush(); + return baos.toByteArray(); + } + + private static void writeUtf8(DataOutputStream out, String s) throws IOException { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + out.writeShort(bytes.length); + out.write(bytes); + } + private static ByteBuffer urlToByteBuffer(URL url) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (InputStream inputStream = url.openStream()) { diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java index 15febb27ff..f6cf623f8b 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,12 +111,13 @@ static class HackClone tableLayoutClass = classLoader.loadClass("org.drjekyll.adocfmt.TableLayout"); + @SuppressWarnings({"rawtypes", "unchecked"}) + final Object layoutEnum = Enum.valueOf((Class) tableLayoutClass, config.tableLayout.toUpperCase(Locale.ROOT)); + builder = builderClass.getMethod("tableLayout", tableLayoutClass).invoke(builder, layoutEnum); + } + builder = builderClass.getMethod("tableMaxLineWidth", int.class).invoke(builder, config.tableMaxLineWidth); + if (config.tableBlankLines != null) { + final Class tableBlankLinesClass = classLoader.loadClass("org.drjekyll.adocfmt.TableBlankLines"); + @SuppressWarnings({"rawtypes", "unchecked"}) + final Object blankLinesEnum = Enum.valueOf((Class) tableBlankLinesClass, config.tableBlankLines.toUpperCase(Locale.ROOT)); + builder = builderClass.getMethod("tableBlankLines", tableBlankLinesClass).invoke(builder, blankLinesEnum); + } final Object adocfmtConfig = builderClass.getMethod("build").invoke(builder); final Object formatter = formatterClass.getConstructor(configClass).newInstance(adocfmtConfig); diff --git a/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java index 39049fa20e..fb30afe1d2 100644 --- a/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.List; import java.util.Objects; import com.diffplug.spotless.FormatterFunc; @@ -30,7 +31,7 @@ public final class GherkinUtilsStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String MAVEN_COORDINATE = "io.cucumber:gherkin-utils:"; - private static final String DEFAULT_VERSION = "10.0.0"; + private static final String GHERKIN_MAVEN_COORDINATE = "io.cucumber:gherkin:"; public static final String NAME = "gherkinUtils"; private final JarState.Promised jarState; @@ -42,14 +43,17 @@ private GherkinUtilsStep(JarState.Promised jarState, GherkinUtilsConfig gherkinS } public static String defaultVersion() { - return DEFAULT_VERSION; + return GherkinBuildConfig.VERSION_GHERKIN_UTILS; } public static FormatterStep create(GherkinUtilsConfig gherkinSimpleConfig, String formatterVersion, Provisioner provisioner) { Objects.requireNonNull(provisioner, "provisioner cannot be null"); + List coordinates = List.of( + MAVEN_COORDINATE + formatterVersion, + GHERKIN_MAVEN_COORDINATE + GherkinBuildConfig.VERSION_GHERKIN); return FormatterStep.create(NAME, - new GherkinUtilsStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + formatterVersion, provisioner)), gherkinSimpleConfig), + new GherkinUtilsStep(JarState.promise(() -> JarState.from(coordinates, provisioner)), gherkinSimpleConfig), GherkinUtilsStep::equalityState, GherkinUtilsStep.State::toFormatter); } diff --git a/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java b/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java index 5f181d029c..756db6c405 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java @@ -49,7 +49,7 @@ public final class CleanthatJavaStep implements Serializable { /** * CleanThat changelog is available at here. */ - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "2.25"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, BuildConfig.VERSION_CLEANTHAT); private final JarState.Promised jarState; private final String version; diff --git a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java index ef1b3fd467..2ee089c751 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ExpandWildcardImportsStep.java @@ -38,7 +38,7 @@ public final class ExpandWildcardImportsStep implements Serializable { private static final long serialVersionUID = 1L; private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Java-Parser; maybe you set an incompatible version?"; - private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core:3.27.1"; + private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-symbol-solver-core:" + BuildConfig.VERSION_JAVAPARSER; private final Collection typeSolverClasspath; private final JarState.Promised jarState; diff --git a/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java b/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java index c0ca7ad0ce..079955a731 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,7 +121,9 @@ private static FormatterStep createInternally(String name, String groupArtifact, private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME) .addMin(21, "1.17.0") // java 21 requires at least 1.17.0 due to https://github.com/google/google-java-format/issues/898 - .add(17, "1.28.0"); // default version + .addMin(25, "1.30.0") // import module (JEP 511) requires google-java-format >= 1.30.0 (https://github.com/google/google-java-format/issues/1213) + .add(17, "1.28.0") // last version compatible with JDK 17 (1.30.0 references JCAnyPattern, JDK 21+ only) + .add(21, BuildConfig.VERSION_GJF); // default on JVM 21+ (1.30.0+ handles `import module` in RemoveUnusedImports on JVM 25+) public static String defaultGroupArtifact() { return MAVEN_COORDINATE; diff --git a/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java b/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java index f51e0356d0..5840cd4073 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ public final class PalantirJavaFormatStep implements Serializable { private static final String DEFAULT_STYLE = "PALANTIR"; private static final String NAME = "palantir-java-format"; public static final String MAVEN_COORDINATE = "com.palantir.javaformat:palantir-java-format:"; - public static final String DEFAULT_VERSION = "2.80.0"; // compatible with Java 11+ + public static final String DEFAULT_VERSION = BuildConfig.VERSION_PALANTIR_JAVA_FORMAT; // compatible with Java 11+ /** The jar that contains the formatter. */ private final JarState.Promised jarState; diff --git a/lib/src/main/java/com/diffplug/spotless/java/PrinceOfSpaceStep.java b/lib/src/main/java/com/diffplug/spotless/java/PrinceOfSpaceStep.java new file mode 100644 index 0000000000..d7d519efb3 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/PrinceOfSpaceStep.java @@ -0,0 +1,159 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.java; + +import java.io.Serial; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.Provisioner; + +/** Wraps up prince-of-space as a FormatterStep. */ +public final class PrinceOfSpaceStep implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private static final String NAME = "prince-of-space"; + public static final String MAVEN_COORDINATE = "io.github.agustafson.princeofspace:prince-of-space-core:"; + public static final String DEFAULT_VERSION = BuildConfig.VERSION_PRINCE_OF_SPACE; + + /** The jar that contains the formatter. */ + private final JarState.Promised jarState; + /** Version of the formatter jar. */ + private final String formatterVersion; + @Nullable private final Options options; + + private PrinceOfSpaceStep(JarState.Promised jarState, String formatterVersion, @Nullable Options options) { + this.jarState = jarState; + this.formatterVersion = formatterVersion; + this.options = options; + } + + /** Creates a step which formats Java source with prince-of-space's default options. */ + public static FormatterStep create(Provisioner provisioner) { + return create(defaultVersion(), provisioner); + } + + /** Creates a step which formats Java source with prince-of-space's default options. */ + public static FormatterStep create(String version, Provisioner provisioner) { + return create(version, provisioner, null); + } + + /** Creates a step which formats Java source with the given options. */ + public static FormatterStep create(String version, Provisioner provisioner, @Nullable Options options) { + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(provisioner, "provisioner"); + return FormatterStep.create(NAME, + new PrinceOfSpaceStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), version, options), + PrinceOfSpaceStep::equalityState, + State::createFormat); + } + + /** Get default formatter version */ + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + private State equalityState() { + return new State(jarState.get(), formatterVersion, options); + } + + /** Mutable bag of the optional prince-of-space {@code FormatterConfig} knobs; unset (null) fields keep prince-of-space's own defaults. */ + public static class Options implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @Nullable private String indentStyle; + @Nullable private Integer indentSize; + @Nullable private Integer lineLength; + @Nullable private String wrapStyle; + @Nullable private Boolean closingParenOnNewLine; + @Nullable private Boolean trailingCommas; + @Nullable private Integer javaLanguageLevel; + + public Options() {} + + public void setIndentStyle(String indentStyle) { + this.indentStyle = indentStyle; + } + + public void setIndentSize(int indentSize) { + this.indentSize = indentSize; + } + + public void setLineLength(int lineLength) { + this.lineLength = lineLength; + } + + public void setWrapStyle(String wrapStyle) { + this.wrapStyle = wrapStyle; + } + + public void setClosingParenOnNewLine(boolean closingParenOnNewLine) { + this.closingParenOnNewLine = closingParenOnNewLine; + } + + public void setTrailingCommas(boolean trailingCommas) { + this.trailingCommas = trailingCommas; + } + + public void setJavaLanguageLevel(int javaLanguageLevel) { + this.javaLanguageLevel = javaLanguageLevel; + } + } + + private static final class State implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private final JarState jarState; + private final String formatterVersion; + @Nullable private final Options options; + + State(JarState jarState, String formatterVersion, @Nullable Options options) { + if (Jvm.version() < 17) { + throw new IllegalStateException("prince-of-space requires a JDK 17+ host runtime, this JVM is " + Jvm.version()); + } + this.jarState = jarState; + this.formatterVersion = formatterVersion; + this.options = options; + } + + FormatterFunc createFormat() throws Exception { + final ClassLoader classLoader = jarState.getClassLoader(); + final Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.pos.PrinceOfSpaceFormatterFunc"); + final Constructor constructor = formatterFunc.getConstructor( + String.class, Integer.class, Integer.class, String.class, Boolean.class, Boolean.class, Integer.class); + if (options == null) { + return (FormatterFunc) constructor.newInstance(null, null, null, null, null, null, null); + } + return (FormatterFunc) constructor.newInstance( + options.indentStyle, + options.indentSize, + options.lineLength, + options.wrapStyle, + options.closingParenOnNewLine, + options.trailingCommas, + options.javaLanguageLevel); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStep.java b/lib/src/main/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStep.java new file mode 100644 index 0000000000..1c247b218a --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStep.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.java; + +import static com.diffplug.spotless.JarState.from; +import static com.diffplug.spotless.JarState.promise; +import static java.util.Objects.requireNonNull; + +import java.io.Serial; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +/** + * Replaces fully qualified type names with simple names and adds the necessary imports. + * Uses JavaParser to identify type references in the AST, avoiding false positives + * in strings, comments, annotations, and other non-type contexts. + * + *

Designed to run before {@code importOrder()} and {@code removeUnusedImports()}. + */ +public final class ShortenFullyQualifiedTypesStep implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private static final String NAME = "shortenFullyQualifiedTypes"; + private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with JavaParser; maybe you set an incompatible version?"; + private static final String MAVEN_COORDINATES = "com.github.javaparser:javaparser-core:" + BuildConfig.VERSION_JAVAPARSER; + + private final JarState.Promised jarState; + + private ShortenFullyQualifiedTypesStep(JarState.Promised jarState) { + this.jarState = jarState; + } + + public static FormatterStep create(Provisioner provisioner) { + requireNonNull(provisioner); + return FormatterStep.create(NAME, + new ShortenFullyQualifiedTypesStep(promise(() -> from(MAVEN_COORDINATES, provisioner))), + ShortenFullyQualifiedTypesStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get()); + } + + private record State(JarState jarState) implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + FormatterFunc toFormatter() { + try { + return (FormatterFunc) jarState + .getClassLoader() + .loadClass("com.diffplug.spotless.glue.javaparser.ShortenQualifiedTypesFormatterFunc") + .getConstructor() + .newInstance(); + } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException + | InstantiationException | IllegalAccessException | NoClassDefFoundError cause) { + throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause); + } + } +}} diff --git a/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java index 14ab3b1f4d..e4e19bdaab 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java @@ -39,7 +39,6 @@ public final class TableTestFormatterStep implements Serializable { private static final long serialVersionUID = 2L; private static final String NAME = "tableTestFormatter"; private static final String MAVEN_COORDINATE = "org.tabletest:tabletest-formatter-core:"; - private static final String DEFAULT_VERSION = "1.1.1"; /** Default fallback indent style ({@code "space"}) for Java/Kotlin files. */ public static final String DEFAULT_INDENT_STYLE = "space"; @@ -89,7 +88,7 @@ public static FormatterStep create(String version, Provisioner provisioner, Stri /** Get default formatter version. */ public static String defaultVersion() { - return DEFAULT_VERSION; + return BuildConfig.VERSION_TABLETEST_FORMATTER; } private State equalityState() { diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java index 3b8ef48026..deb37b0bb6 100644 --- a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java +++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ public final class JacksonJsonStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String MAVEN_COORDINATE = "com.fasterxml.jackson.core:jackson-databind:"; - private static final String DEFAULT_VERSION = "2.20.1"; public static final String NAME = "jacksonJson"; private final JarState.Promised jarState; @@ -46,7 +45,7 @@ private JacksonJsonStep(JarState.Promised jarState, JacksonConfig jacksonConfig) } public static String defaultVersion() { - return DEFAULT_VERSION; + return JsonBuildConfig.VERSION_JACKSON; } public static FormatterStep create(Provisioner provisioner) { diff --git a/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java b/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java index dce0c3a307..4d293d86a8 100644 --- a/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java +++ b/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ public final class JsonPatchStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String MAVEN_COORDINATE = "com.flipkart.zjsonpatch:zjsonpatch"; - private static final String DEFAULT_VERSION = "0.4.16"; public static final String NAME = "apply-json-patch"; private final JarState.Promised jarState; @@ -49,8 +48,12 @@ private JsonPatchStep(JarState.Promised jarState, this.patch = patch; } + public static String defaultVersion() { + return JsonBuildConfig.VERSION_ZJSONPATCH; + } + public static FormatterStep create(String patchString, Provisioner provisioner) { - return create(DEFAULT_VERSION, patchString, provisioner); + return create(JsonBuildConfig.VERSION_ZJSONPATCH, patchString, provisioner); } public static FormatterStep create(String zjsonPatchVersion, String patchString, Provisioner provisioner) { @@ -64,7 +67,7 @@ public static FormatterStep create(String zjsonPatchVersion, String patchString, } public static FormatterStep create(List> patch, Provisioner provisioner) { - return create(DEFAULT_VERSION, patch, provisioner); + return create(JsonBuildConfig.VERSION_ZJSONPATCH, patch, provisioner); } public static FormatterStep create(String zjsonPatchVersion, List> patch, Provisioner provisioner) { diff --git a/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java index 3fbe8ae7b9..70920d7553 100644 --- a/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java +++ b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ public final class JsonSimpleStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String MAVEN_COORDINATE = "org.json:json:"; - private static final String DEFAULT_VERSION = "20210307"; public static final String NAME = "jsonSimple"; private final JarState.Promised jarState; @@ -48,11 +47,15 @@ private JsonSimpleStep(JarState.Promised jarState, int indentSpaces) { public static FormatterStep create(int indent, Provisioner provisioner) { Objects.requireNonNull(provisioner, "provisioner cannot be null"); return FormatterStep.create(NAME, - new JsonSimpleStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + DEFAULT_VERSION, provisioner)), indent), + new JsonSimpleStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + defaultVersion(), provisioner)), indent), JsonSimpleStep::equalityState, State::toFormatter); } + public static String defaultVersion() { + return JsonBuildConfig.VERSION_JSON_SIMPLE; + } + private State equalityState() { return new State(jarState.get(), indentSpaces); } diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java index 5ad4034bb6..b7e14f749f 100644 --- a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java +++ b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2025 DiffPlug + * Copyright 2022-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ public final class GsonStep implements Serializable { private static final String MAVEN_COORDINATES = "com.google.code.gson:gson"; private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?"; public static final String NAME = "gson"; - public static final String DEFAULT_VERSION = "2.13.2"; + public static final String DEFAULT_VERSION = GsonBuildConfig.VERSION_GSON; private final JarState.Promised jarState; private final GsonConfig gsonConfig; diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java index 8efe5a61b7..a63c9bc4d5 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,13 +49,12 @@ private DiktatStep(JarState.Promised jarState, String versionDiktat, boolean isS private static final String PACKAGE_RELOCATED_VERSION = "2.0.0"; - private static final String DEFAULT_VERSION = "2.0.0"; private static final String NAME = "diktat"; private static final String MAVEN_COORDINATE_PRE_2_0_0 = "org.cqfn.diktat:diktat-rules:"; private static final String MAVEN_COORDINATE = "com.saveourtool.diktat:diktat-runner:"; public static String defaultVersionDiktat() { - return DEFAULT_VERSION; + return KotlinBuildConfig.VERSION_DIKTAT; } public static FormatterStep create(Provisioner provisioner) { diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java index 1da50be59e..558b20e1e7 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ public final class KtLintStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - private static final String DEFAULT_VERSION = "1.8.0"; + private static final String NAME = "ktlint"; private static final String MAVEN_COORDINATE_0_DOT = "com.pinterest:ktlint:"; private static final String MAVEN_COORDINATE_1_DOT = "com.pinterest.ktlint:ktlint-cli:"; @@ -83,7 +83,7 @@ public static FormatterStep create(String version, } public static String defaultVersion() { - return DEFAULT_VERSION; + return KotlinBuildConfig.VERSION_KTLINT; } private State equalityState() { diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java index 689332d2a7..c3e63f7447 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java @@ -36,14 +36,18 @@ import com.diffplug.spotless.ThrowingEx; /** - * Wraps up ktfmt as a FormatterStep. + * Wraps up ktfmt as a FormatterStep. */ public final class KtfmtStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - private static final String DEFAULT_VERSION = "0.63"; + private static final String NAME = "ktfmt"; - private static final String MAVEN_COORDINATE = "com.facebook:ktfmt:"; + /** + * Since 0.65, ktfmt is published under the {@code org.jetbrains.kotlinx} group with the {@code org.jetbrains.kotlinx.ktfmt} package. + */ + private static final String MAVEN_COORDINATE = "org.jetbrains.kotlinx:ktfmt:"; + private static final String MAVEN_COORDINATE_LEGACY = "com.facebook:ktfmt:"; private final String version; /** @@ -69,7 +73,7 @@ private KtfmtStep(String version, /** * Used to allow multiple style option through formatting options and since when is each of them available. * - * @see ktfmt source + * @see ktfmt source */ public enum Style { // @formatter:off @@ -213,13 +217,24 @@ public static FormatterStep create(String version, Provisioner provisioner, @Nul Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); return FormatterStep.create(NAME, - new KtfmtStep(version, JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), style, options), + new KtfmtStep(version, JarState.promise(() -> JarState.from(mavenCoordinate(version) + version, provisioner)), style, options), KtfmtStep::equalityState, State::createFormat); } public static String defaultVersion() { - return DEFAULT_VERSION; + return KotlinBuildConfig.VERSION_KTFMT; + } + + private static String mavenCoordinate(String version) { + return isLegacyPackage(version) ? MAVEN_COORDINATE_LEGACY : MAVEN_COORDINATE; + } + + /** + * Versions before 0.65 use the {@code com.facebook} group and package, so they can't use the glue code compiled against the latest ktfmt. + */ + private static boolean isLegacyPackage(String version) { + return BadSemver.version(version) < BadSemver.version(0, 65); } private State equalityState() { @@ -249,11 +264,7 @@ private static final class State implements Serializable { FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); - if (BadSemver.version(version) < BadSemver.version(0, 51)) { - return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); - } - - if (options != null && BadSemver.version(version) < BadSemver.version(0, 63)) { + if (isLegacyPackage(version)) { return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); } @@ -342,13 +353,16 @@ private String getKtfmtStyleOption(Style style) { } } + /** + * Reflection-based formatter for ktfmt versions before 0.65, which use the {@code com.facebook.ktfmt} package. + */ private static final class KtfmtFormatterFuncCompat { private static final String PACKAGE = "com.facebook.ktfmt"; /** * The format method is available in the link below. * - * @see ktfmt source + * @see ktfmt source */ static final String FORMATTER_METHOD = "format"; @@ -430,7 +444,7 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* manageTrailingCommas = */ Objects.requireNonNullElse(getManageTrailingCommasFrom(options.trailingCommaManagementStrategy), (Boolean) formattingOptionsClass.getMethod("getManageTrailingCommas").invoke(formattingOptions)), /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); - } else { + } else if (BadSemver.version(version) < BadSemver.version(0, 63)) { Class trailingCommaManagementStrategyClass = getTrailingCommaManagementStrategyClazz(); Object trailingCommaManagementStrategy = options.trailingCommaManagementStrategy == null ? formattingOptionsClass.getMethod("getTrailingCommaManagementStrategy").invoke(formattingOptions) @@ -442,6 +456,28 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* trailingCommaManagementStrategy = */ trailingCommaManagementStrategy, /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); + } else { + // FormattingOptions.Builder is available since 0.63, and keeps the options not managed here (e.g. preserveLambdaBreaks) from the style. + Object builder = formattingOptionsClass.getMethod("toBuilder").invoke(formattingOptions); + Class builderClass = builder.getClass(); + if (options.maxWidth != null) { + builderClass.getMethod("maxWidth", int.class).invoke(builder, options.maxWidth); + } + if (options.blockIndent != null) { + builderClass.getMethod("blockIndent", int.class).invoke(builder, options.blockIndent); + } + if (options.continuationIndent != null) { + builderClass.getMethod("continuationIndent", int.class).invoke(builder, options.continuationIndent); + } + if (options.trailingCommaManagementStrategy != null) { + Class trailingCommaManagementStrategyClass = getTrailingCommaManagementStrategyClazz(); + builderClass.getMethod("trailingCommaManagementStrategy", trailingCommaManagementStrategyClass).invoke(builder, + Enum.valueOf((Class) trailingCommaManagementStrategyClass, options.trailingCommaManagementStrategy.name())); + } + if (options.removeUnusedImports != null) { + builderClass.getMethod("removeUnusedImports", boolean.class).invoke(builder, options.removeUnusedImports); + } + formattingOptions = builderClass.getMethod("build").invoke(builder); } } @@ -495,15 +531,14 @@ private Class getTrailingCommaManagementStrategyClazz() throws Exception { } private @Nullable Boolean getManageTrailingCommasFrom( - @Nullable TrailingCommaManagementStrategy trailingCommaManagementStrategy - ) { + @Nullable TrailingCommaManagementStrategy trailingCommaManagementStrategy) { if (trailingCommaManagementStrategy == null) { return null; } return switch (trailingCommaManagementStrategy) { - case NONE, ONLY_ADD -> false; - case COMPLETE -> true; + case NONE, ONLY_ADD -> false; + case COMPLETE -> true; }; } } diff --git a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java index 82848e08ef..24c8db8299 100644 --- a/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java +++ b/lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ public final class FlexmarkStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - private static final String DEFAULT_VERSION = "0.64.8"; private static final String MAVEN_COORDINATE = "com.vladsch.flexmark:flexmark-all:"; public static final String NAME = "flexmark-java"; @@ -57,7 +56,7 @@ public static FormatterStep create(String version, Provisioner provisioner, Flex } public static String defaultVersion() { - return DEFAULT_VERSION; + return MarkdownBuildConfig.VERSION_FLEXMARK; } private State equalityState() { diff --git a/lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java b/lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java index b08148d8c8..bde620c172 100644 --- a/lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java +++ b/lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ public final class FreshMarkStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - private static final String DEFAULT_VERSION = "1.3.1"; private static final String NAME = "freshmark"; private static final String MAVEN_COORDINATE = "com.diffplug.freshmark:freshmark:"; @@ -85,7 +84,7 @@ public static FormatterStep create(String version, Map properties, Pr } public static String defaultVersion() { - return DEFAULT_VERSION; + return MarkdownBuildConfig.VERSION_FRESHMARK; } private State equalityState() throws Exception { diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java index 8244a36bfb..e17446552a 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ private static class State extends NpmFormatterStepStateBase implements Serializ super(stepName, new NpmConfig( replaceDevDependencies( - NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/eslint-package.json"), + NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/eslint-package.json.template"), new TreeMap<>(devDependencies)), NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/common-serve.js", diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java index c483e0c1e2..d7ecc0ef7e 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package com.diffplug.spotless.npm; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -43,14 +46,12 @@ private NodeModulesCachingNpmProcessFactory(@Nonnull File cacheDir) { this.shadowCopy = new ShadowCopy(this::assertDir); } - private synchronized File assertDir() { - if (cacheDir.exists() && !cacheDir.isDirectory()) { - throw new IllegalArgumentException("Cache dir must be a directory"); - } - if (!cacheDir.exists()) { - if (!cacheDir.mkdirs()) { - throw new IllegalArgumentException("Cache dir could not be created."); - } + private File assertDir() { + try { + Files.createDirectories(cacheDir.toPath()); + } catch (IOException e) { + throw new UncheckedIOException( + "Could not create cache directory: " + cacheDir.getAbsolutePath(), e); } return cacheDir; } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java index e272b2ebdf..e12107d971 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ private static class State extends NpmFormatterStepStateBase implements Serializ super(stepName, new NpmConfig( replaceDevDependencies( - NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"), + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json.template"), new TreeMap<>(devDependencies)), NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/common-serve.js", diff --git a/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java b/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java index 7b95db7740..a7f0313ce4 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -129,10 +129,11 @@ public NpmServe(File workingDir, NpmFormatterStepLocations formatterStepLocation @Override protected List commandLine() { + // npm 12 rejects --scripts-prepend-node-path (unknown CLI flag). Node is already + // on PATH via environmentVariables(), which is what that flag used to do. return List.of( npmExecutable(), "start", - "--scripts-prepend-node-path=true", "--", "--node-server-instance-id=" + nodeServerInstanceId); } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java index 2e9fc13b58..34bb39795e 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ public static class State extends NpmFormatterStepStateBase implements Serializa public State(String stepName, Map versions, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) throws IOException { super(stepName, new NpmConfig( - replaceDevDependencies(NpmResourceHelper.readUtf8StringFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), new TreeMap<>(versions)), + replaceDevDependencies(NpmResourceHelper.readUtf8StringFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json.template"), new TreeMap<>(versions)), NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/common-serve.js", "/com/diffplug/spotless/npm/tsfmt-serve.js"), diff --git a/lib/src/main/java/com/diffplug/spotless/pom/SortPomCfg.java b/lib/src/main/java/com/diffplug/spotless/pom/SortPomCfg.java index 9825cfa4d8..5054ed2238 100644 --- a/lib/src/main/java/com/diffplug/spotless/pom/SortPomCfg.java +++ b/lib/src/main/java/com/diffplug/spotless/pom/SortPomCfg.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ public class SortPomCfg implements Serializable { @Serial private static final long serialVersionUID = 1L; - public String version = "4.0.0"; + public String version = PomBuildConfig.VERSION_SORTPOM; public String encoding = "UTF-8"; diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java index db63902f2d..5b540b09ec 100644 --- a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,6 +86,9 @@ public Builder verify() { } public RdfFormatterConfig build() { + if (config.turtleFormatterVersion == null) { + config.turtleFormatterVersion = RdfFormatterStep.getDefaultFormatterVersion(); + } return config; } } @@ -98,7 +101,7 @@ public boolean equals(Object o) { if (!(o instanceof RdfFormatterConfig)) { return false; } - RdfFormatterConfig that = (RdfFormatterConfig) o; + var that = (RdfFormatterConfig) o; return isFailOnWarning() == that.isFailOnWarning() && Objects.equals(turtleFormatterVersion, that.turtleFormatterVersion); } diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java index dce13ea0f5..88afa3b5a8 100644 --- a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,21 @@ import java.util.Objects; import java.util.TreeMap; +import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; public class RdfFormatterStep implements Serializable { - public static final String LATEST_TURTLE_FORMATTER_VERSION = "1.2.13"; + public static final String LATEST_TURTLE_FORMATTER_VERSION = RdfBuildConfig.VERSION_COOL_RDF_FORMATTER; @Serial private static final long serialVersionUID = 1L; - private static final String TURTLE_FORMATTER_COORDINATES = "de.atextor:turtle-formatter"; + private static final Jvm.Support JVM_SUPPORT = Jvm. support("cool-rdf-formatter") + .add(25, RdfBuildConfig.VERSION_COOL_RDF_FORMATTER); + + private static final String TURTLE_FORMATTER_COORDINATES = "cool.rdf:cool-rdf-formatter"; private final JarState.Promised jarState; private final Map turtleFormatterStyle; @@ -49,9 +54,10 @@ public static State state(RdfFormatterStep step) { return new State(step.config, step.turtleFormatterStyle, step.jarState.get()); } - public static RdfFormatterFunc formatterFunc(State state) + public static FormatterFunc formatterFunc(State state) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { - return new RdfFormatterFunc(state); + var formatterFunc = new RdfFormatterFunc(state); + return JVM_SUPPORT.suggestLaterVersionOnError(state.config.getTurtleFormatterVersion(), formatterFunc); } public RdfFormatterStep(JarState.Promised jarState, RdfFormatterConfig config, @@ -59,6 +65,11 @@ public RdfFormatterStep(JarState.Promised jarState, RdfFormatterConfig config, this.jarState = jarState; this.turtleFormatterStyle = turtleFormatterStyle; this.config = config; + JVM_SUPPORT.assertFormatterSupported(this.config.getTurtleFormatterVersion()); + } + + static String getDefaultFormatterVersion() { + return JVM_SUPPORT.getMinimumRequiredFormatterVersion(); } public static class State implements Serializable { @@ -98,7 +109,7 @@ public boolean equals(Object o) { if (!(o instanceof State)) { return false; } - State state = (State) o; + var state = (State) o; return Objects.equals(getConfig(), state.getConfig()) && Objects.equals( getTurtleFormatterStyle(), state.getTurtleFormatterStyle()) && Objects.equals( diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java b/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java index 061062434f..eba6993ef8 100644 --- a/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java +++ b/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.io.File; import java.io.StringWriter; import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -64,7 +63,8 @@ class ReflectionHelper { private final Class TurtleFormatFormattingStyleClass; private final Class TurtleFormatFormattingStyleBuilderClass; private final Class TurtleFormatFormatterClass; - private final Class TurtleFormatKnownPrefix; + private final Class CoolRdfPrefixesClass; + private final Class CoolRdfPrefixClass; private final Method graphStream; private final Method graphFindTriple; @@ -96,12 +96,11 @@ public ReflectionHelper(RdfFormatterStep.State state) this.JenaModelFactoryClass = classLoader.loadClass("org.apache.jena.rdf.model.ModelFactory"); this.JenaLangClass = classLoader.loadClass("org.apache.jena.riot.Lang"); this.JenaRDFFormatClass = classLoader.loadClass("org.apache.jena.riot.RDFFormat"); - this.TurtleFormatFormatterClass = classLoader.loadClass("de.atextor.turtle.formatter.TurtleFormatter"); - this.TurtleFormatFormattingStyleClass = classLoader.loadClass("de.atextor.turtle.formatter.FormattingStyle"); - Class[] innerClasses = TurtleFormatFormattingStyleClass.getDeclaredClasses(); - this.TurtleFormatFormattingStyleBuilderClass = Arrays.stream(innerClasses) - .filter(c -> "FormattingStyleBuilder".equals(c.getSimpleName())).findFirst().orElseThrow(); - this.TurtleFormatKnownPrefix = Arrays.stream(innerClasses).filter(c -> "KnownPrefix".equals(c.getSimpleName())).findFirst().orElseThrow(); + this.TurtleFormatFormatterClass = classLoader.loadClass("cool.rdf.formatter.TurtleFormatter"); + this.TurtleFormatFormattingStyleClass = classLoader.loadClass("cool.rdf.formatter.FormattingStyle"); + this.TurtleFormatFormattingStyleBuilderClass = TurtleFormatFormattingStyleClass.getMethod("builder").getReturnType(); + this.CoolRdfPrefixesClass = classLoader.loadClass("cool.rdf.core.Prefixes"); + this.CoolRdfPrefixClass = classLoader.loadClass("cool.rdf.core.model.RdfPrefix"); this.getSubject = JenaStatementClass.getMethod("getSubject"); this.getPredicate = JenaStatementClass.getMethod("getPredicate"); this.getObject = JenaStatementClass.getMethod("getObject"); @@ -391,7 +390,7 @@ private Object instantiate(Type type, String stringRepresentation) .invoke(this.jenaModelInstance, namespace, localname); } } - if (type.equals(TurtleFormatKnownPrefix)) { + if (type.equals(CoolRdfPrefixClass)) { return getKnownPrefix(stringRepresentation); } throw new IllegalArgumentException("Cannot instantiate class %s from string representation %s".formatted(type, stringRepresentation)); @@ -404,7 +403,7 @@ private String tryToMakeUri(String stringRepresentation) //could be a known prefix String prefix = stringRepresentation.substring(0, colonIndex); Object knownPrefix = getKnownPrefix(prefix); - String base = this.TurtleFormatKnownPrefix.getMethod("iri").invoke(knownPrefix).toString(); + String base = this.CoolRdfPrefixClass.getMethod("uri").invoke(knownPrefix).toString(); return base + stringRepresentation.substring(colonIndex + 1); } // try to parse a URI - throws an IllegalArgumentException if it is not a URI @@ -414,20 +413,16 @@ private String tryToMakeUri(String stringRepresentation) private Object getKnownPrefix(String stringRepresentation) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { - Field[] fields = TurtleFormatFormattingStyleClass.getDeclaredFields(); List options = new ArrayList<>(); - for (Field field : fields) { - if (field.getType().equals(TurtleFormatKnownPrefix)) { - Object knownPrefix = field.get(TurtleFormatFormattingStyleClass); - String prefix = (String) TurtleFormatKnownPrefix.getMethod("prefix").invoke(knownPrefix); - options.add(prefix); - if (stringRepresentation.equals(prefix)) { - return knownPrefix; - } + for (Object knownPrefix : CoolRdfPrefixesClass.getEnumConstants()) { + String prefix = (String) CoolRdfPrefixClass.getMethod("prefix").invoke(knownPrefix); + options.add(prefix); + if (stringRepresentation.equals(prefix)) { + return knownPrefix; } } - throw new IllegalArgumentException("Unable to find FormattingStyle.KnownPrefix for prefix '%s'. Options are: %s".formatted(stringRepresentation, options.stream().collect( - Collectors.joining(",\n\t", "\n\t", "\n")))); + throw new IllegalArgumentException("Unable to find a known Cool RDF prefix for '%s'. Options are: %s".formatted(stringRepresentation, options.stream().collect( + Collectors.joining(", ")))); } private static String[] split(String parameterValueAsString) { @@ -439,7 +434,7 @@ private static String[] split(String parameterValueAsString) { private Method getBuilderMethod(String optionName) { Method[] allMethods = TurtleFormatFormattingStyleBuilderClass.getDeclaredMethods(); - List methods = Arrays.stream(allMethods).filter(m -> m.getName().equals(optionName)) + List methods = Arrays.stream(allMethods).filter(m -> m.getName().equals(optionName) && m.getParameterCount() == 1) .collect( Collectors.toList()); if (methods.isEmpty()) { diff --git a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java index 9e045b41d6..b04d0f2cc4 100644 --- a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java @@ -38,7 +38,7 @@ public final class ScalaFmtStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - static final String DEFAULT_VERSION = "3.8.1"; + static final String DEFAULT_VERSION = ScalaBuildConfig.VERSION_SCALAFMT; private static final String DEFAULT_SCALA_MAJOR_VERSION = "2.13"; private static final String NAME = "scalafmt"; diff --git a/lib/src/main/java/com/diffplug/spotless/toml/TODO.md b/lib/src/main/java/com/diffplug/spotless/toml/TODO.md index f43d70bfe1..8bd7eb2e05 100644 --- a/lib/src/main/java/com/diffplug/spotless/toml/TODO.md +++ b/lib/src/main/java/com/diffplug/spotless/toml/TODO.md @@ -11,6 +11,10 @@ Tracked issues found during review against the TOML v1.1.0 spec (https://toml.io - [x] Multi-line inline tables are not parsed correctly (split across lines and corrupted) - [x] Long lines can be split into multi-line inline tables (configurable `maxLineLength`) - [x] Short multi-line inline tables are joined into single lines when they fit +- [x] Commas inside literal strings and triple-quoted strings are preserved by `splitTopLevel` +- [x] Escaped backslashes before closing quotes no longer confuse `splitTopLevel` +- [x] Single-line strings cannot consume following entries across a newline; unfinished + entries report a lint at their starting line instead of returning a partial catalog ## TODO โ€” TOML spec edge cases @@ -27,10 +31,9 @@ addressed for full TOML spec compliance. (`[section.subsection]`) and quoted table headers (`["quoted.key"]`). Their entries are silently dropped. -### String handling in `splitTopLevel` +### Scanner and formatting improvements -- [ ] Single-quoted (literal) strings `'...'` are not recognized โ€” commas or `=` inside - them will incorrectly split or match. -- [ ] Multiline string delimiters (`"""`, `'''`) confuse the single-char quote toggle. -- [ ] Double-backslash before closing quote (`"value\\"`) is misidentified as an escaped - quote. Needs odd/even backslash counting instead of single-char lookbehind. +- [ ] Carry scanner state across lines instead of rescanning the accumulated multiline + entry after each line; the current approach is quadratic for long entries. +- [ ] Normalize trailing whitespace outside strings in preserved multiline entries; + whitespace inside multiline strings must remain unchanged. diff --git a/lib/src/main/java/com/diffplug/spotless/toml/VersionCatalogStep.java b/lib/src/main/java/com/diffplug/spotless/toml/VersionCatalogStep.java index 9faafa54e8..3a4e2002a2 100644 --- a/lib/src/main/java/com/diffplug/spotless/toml/VersionCatalogStep.java +++ b/lib/src/main/java/com/diffplug/spotless/toml/VersionCatalogStep.java @@ -28,6 +28,7 @@ import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.Lint; public final class VersionCatalogStep { private VersionCatalogStep() {} @@ -58,7 +59,7 @@ static String format(String raw, boolean stripQuotedKeys) { return raw; } - Map> sections = parseSections(raw); + Map sections = parseSections(raw); List preambleLines = extractPreamble(raw); StringBuilder result = new StringBuilder(); @@ -82,7 +83,8 @@ static String format(String raw, boolean stripQuotedKeys) { } for (String header : orderedKeys) { - List entries = sections.get(header); + Section section = sections.get(header); + List entries = section.entries; if (!first) { result.append('\n'); } @@ -100,6 +102,9 @@ static String format(String raw, boolean stripQuotedKeys) { } result.append(entry.formatted).append('\n'); } + for (String commentLine : section.trailingComments) { + result.append(commentLine).append('\n'); + } } return result.toString(); @@ -119,21 +124,24 @@ private static List extractPreamble(String raw) { return preamble; } - private static Map> parseSections(String raw) { - Map> sections = new LinkedHashMap<>(); + private static Map parseSections(String raw) { + Map sections = new LinkedHashMap<>(); String currentHeader = null; - List currentEntries = null; + Section currentSection = null; List pendingComments = new ArrayList<>(); StringBuilder multiLineAccumulator = null; + int lineNumber = 0; + int entryStartLine = 0; for (String line : raw.split("\n", -1)) { + lineNumber++; String trimmed = line.trim(); if (multiLineAccumulator != null) { - multiLineAccumulator.append(' ').append(trimmed); + multiLineAccumulator.append('\n').append(line); if (isBalanced(multiLineAccumulator.toString())) { Entry entry = new Entry(multiLineAccumulator.toString(), new ArrayList<>(pendingComments)); - currentEntries.add(entry); + currentSection.entries.add(entry); pendingComments.clear(); multiLineAccumulator = null; } @@ -145,47 +153,121 @@ private static Map> parseSections(String raw) { } Matcher headerMatcher = TABLE_HEADER.matcher(trimmed); if (headerMatcher.matches()) { + moveTrailingComments(currentSection, pendingComments); currentHeader = "[" + headerMatcher.group(1) + "]"; - currentEntries = new ArrayList<>(); - sections.put(currentHeader, currentEntries); - pendingComments.clear(); - } else if (currentEntries != null) { + currentSection = new Section(); + sections.put(currentHeader, currentSection); + } else if (currentSection != null) { if (trimmed.isEmpty() || trimmed.startsWith("#")) { pendingComments.add(trimmed); } else if (!isBalanced(trimmed)) { - multiLineAccumulator = new StringBuilder(trimmed); + multiLineAccumulator = new StringBuilder(line.stripLeading()); + entryStartLine = lineNumber; } else { Entry entry = new Entry(trimmed, new ArrayList<>(pendingComments)); - currentEntries.add(entry); + currentSection.entries.add(entry); pendingComments.clear(); } } } + if (multiLineAccumulator != null) { + // Report the incomplete entry instead of silently returning a partially parsed catalog. + throw Lint.atLine(entryStartLine, "unterminatedEntry", "Unterminated version catalog entry in " + currentHeader).shortcut(); + } + moveTrailingComments(currentSection, pendingComments); return sections; } + private static void moveTrailingComments(Section section, List pendingComments) { + if (section == null) { + return; + } + int lastNonBlank = pendingComments.size(); + while (lastNonBlank > 0 && pendingComments.get(lastNonBlank - 1).isEmpty()) { + lastNonBlank--; + } + section.trailingComments.addAll(pendingComments.subList(0, lastNonBlank)); + pendingComments.clear(); + } + private static boolean isBalanced(String text) { int depth = 0; - boolean inQuote = false; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); - if (c == '"' && (i == 0 || text.charAt(i - 1) != '\\')) { - inQuote = !inQuote; - } else if (!inQuote) { - if (c == '{' || c == '[') { - depth++; - } else if (c == '}' || c == ']') { - depth--; + if (c == '"' || c == '\'') { + i = skipQuotedString(text, i); + if (i == text.length()) { + return false; } + } else if (c == '#') { + i = text.indexOf('\n', i); + if (i == -1) { + break; + } + } else if (c == '{' || c == '[') { + depth++; + } else if (c == '}' || c == ']') { + depth--; } } return depth == 0; } + /** Returns the closing quote's index, or the text length if the string is unfinished. */ + private static int skipQuotedString(String text, int start) { + char quote = text.charAt(start); + boolean multiline = isMultilineString(text, start); + for (int i = start + (multiline ? 3 : 1); i < text.length(); i++) { + char c = text.charAt(i); + if (!multiline && c == '\n') { + return text.length(); + } + if (quote == '"' && c == '\\' && i + 1 < text.length() && text.charAt(i + 1) != '\n') { + i++; + } else if (c == quote) { + if (!multiline) { + return i; + } + if (i + 2 < text.length() && text.charAt(i + 1) == quote && text.charAt(i + 2) == quote) { + i += 2; + // A multiline string may end with one or two additional literal quotes. + while (i + 1 < text.length() && text.charAt(i + 1) == quote) { + i++; + } + return i; + } + } + } + return text.length(); + } + + /** Called only at an opening quote. */ + private static boolean isMultilineString(String text, int start) { + char quote = text.charAt(start); + return start + 2 < text.length() && text.charAt(start + 1) == quote && text.charAt(start + 2) == quote; + } + + private static boolean hasCommentsOrMultilineStrings(String text) { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '"' || c == '\'') { + if (isMultilineString(text, i)) { + return true; + } + i = skipQuotedString(text, i); + } else if (c == '#') { + return true; + } + } + return false; + } + private static String extractKey(String formattedEntry) { - Matcher matcher = ENTRY_LINE.matcher(formattedEntry); + int lineEnd = formattedEntry.indexOf('\n'); + String firstLine = lineEnd == -1 ? formattedEntry : formattedEntry.substring(0, lineEnd); + Matcher matcher = ENTRY_LINE.matcher(firstLine); if (!matcher.matches()) { return formattedEntry; } @@ -197,7 +279,13 @@ private static String extractKey(String formattedEntry) { } static String formatEntry(String entry, boolean stripQuotedKeys) { - Matcher matcher = ENTRY_LINE.matcher(entry); + int lineEnd = entry.indexOf('\n'); + // Preserve line boundaries that can be significant to comments or multiline strings. + boolean preserveLines = lineEnd != -1 && hasCommentsOrMultilineStrings(entry); + if (lineEnd != -1 && !preserveLines) { + entry = String.join(" ", entry.lines().map(String::trim).toList()); + } + Matcher matcher = ENTRY_LINE.matcher(preserveLines ? entry.substring(0, lineEnd) : entry); if (!matcher.matches()) { return entry; } @@ -209,6 +297,10 @@ static String formatEntry(String entry, boolean stripQuotedKeys) { key = bare; } } + if (preserveLines) { + // The first line starts at offset zero, so the match's value offset also applies to the full entry. + return key + " = " + entry.substring(matcher.start(2)).stripLeading(); + } String valueAndComment = matcher.group(2).trim(); String inlineComment = extractInlineComment(valueAndComment); @@ -225,21 +317,18 @@ static String formatEntry(String entry, boolean stripQuotedKeys) { } private static String extractInlineComment(String valueAndComment) { - boolean inQuote = false; int depth = 0; for (int i = 0; i < valueAndComment.length(); i++) { char c = valueAndComment.charAt(i); - if (c == '"' && (i == 0 || valueAndComment.charAt(i - 1) != '\\')) { - inQuote = !inQuote; - } else if (!inQuote) { - if (c == '{' || c == '[') { - depth++; - } else if (c == '}' || c == ']') { - depth--; - } else if (c == '#' && depth == 0) { - return valueAndComment.substring(i); - } + if (c == '"' || c == '\'') { + i = skipQuotedString(valueAndComment, i); + } else if (c == '{' || c == '[') { + depth++; + } else if (c == '}' || c == ']') { + depth--; + } else if (c == '#' && depth == 0) { + return valueAndComment.substring(i); } } return null; @@ -316,22 +405,19 @@ private static String formatInlineArray(String value) { private static String[] splitTopLevel(String input, char delimiter) { List parts = new ArrayList<>(); int depth = 0; - boolean inQuote = false; int start = 0; for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); - if (c == '"' && (i == 0 || input.charAt(i - 1) != '\\')) { - inQuote = !inQuote; - } else if (!inQuote) { - if (c == '{' || c == '[') { - depth++; - } else if (c == '}' || c == ']') { - depth--; - } else if (c == delimiter && depth == 0) { - parts.add(input.substring(start, i)); - start = i + 1; - } + if (c == '"' || c == '\'') { + i = skipQuotedString(input, i); + } else if (c == '{' || c == '[') { + depth++; + } else if (c == '}' || c == ']') { + depth--; + } else if (c == delimiter && depth == 0) { + parts.add(input.substring(start, i)); + start = i + 1; } } parts.add(input.substring(start)); @@ -345,7 +431,7 @@ private static boolean isBareKey(String key) { } private static final class State implements Serializable { - private static final long serialVersionUID = 3L; + private static final long serialVersionUID = 5L; private final boolean stripQuotedKeys; @@ -372,4 +458,9 @@ String sortKey() { return extractKey(formatted != null ? formatted : content); } } + + private static final class Section { + final List entries = new ArrayList<>(); + final List trailingComments = new ArrayList<>(); + } } diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java index 36b10fa061..1f3cfae576 100644 --- a/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java +++ b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2025 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ public final class JacksonYamlStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String MAVEN_COORDINATE = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:"; - private static final String DEFAULT_VERSION = "2.14.1"; public static final String NAME = "jacksonYaml"; private final JarState.Promised jarState; @@ -47,7 +46,7 @@ private JacksonYamlStep(JarState.Promised jarState, JacksonYamlConfig jacksonCon } public static String defaultVersion() { - return DEFAULT_VERSION; + return YamlBuildConfig.VERSION_JACKSON; } public static FormatterStep create(JacksonYamlConfig jacksonConfig, diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/eslint-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/eslint-package.json.template similarity index 100% rename from lib/src/main/resources/com/diffplug/spotless/npm/eslint-package.json rename to lib/src/main/resources/com/diffplug/spotless/npm/eslint-package.json.template diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json.template similarity index 100% rename from lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json rename to lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json.template diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json.template similarity index 100% rename from lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json rename to lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json.template diff --git a/lib/src/princeOfSpace/java/com/diffplug/spotless/glue/pos/PrinceOfSpaceFormatterFunc.java b/lib/src/princeOfSpace/java/com/diffplug/spotless/glue/pos/PrinceOfSpaceFormatterFunc.java new file mode 100644 index 0000000000..0130e98073 --- /dev/null +++ b/lib/src/princeOfSpace/java/com/diffplug/spotless/glue/pos/PrinceOfSpaceFormatterFunc.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.glue.pos; + +import java.io.File; + +import com.diffplug.spotless.FormatterFunc; + +import io.princeofspace.Formatter; +import io.princeofspace.model.FormatterConfig; +import io.princeofspace.model.IndentStyle; +import io.princeofspace.model.JavaLanguageLevel; +import io.princeofspace.model.WrapStyle; + +public class PrinceOfSpaceFormatterFunc implements FormatterFunc.NeedsFile { + + private final Formatter formatter; + + public PrinceOfSpaceFormatterFunc(String indentStyle, Integer indentSize, Integer lineLength, String wrapStyle, + Boolean closingParenOnNewLine, Boolean trailingCommas, Integer javaLanguageLevel) { + FormatterConfig.Builder builder = FormatterConfig.builder(); + if (indentStyle != null) { + builder.indentStyle(IndentStyle.valueOf(indentStyle)); + } + if (indentSize != null) { + builder.indentSize(indentSize); + } + if (lineLength != null) { + builder.lineLength(lineLength); + } + if (wrapStyle != null) { + builder.wrapStyle(WrapStyle.valueOf(wrapStyle)); + } + if (closingParenOnNewLine != null) { + builder.closingParenOnNewLine(closingParenOnNewLine); + } + if (trailingCommas != null) { + builder.trailingCommas(trailingCommas); + } + if (javaLanguageLevel != null) { + builder.javaLanguageLevel(JavaLanguageLevel.of(javaLanguageLevel)); + } + this.formatter = new Formatter(builder.build()); + } + + @Override + public String applyWithFile(String unix, File file) throws Exception { + return formatter.format(unix, file.toPath()); + } + + @Override + public String toString() { + return "PrinceOfSpaceFormatterFunc{formatter=" + formatter + '}'; + } +} diff --git a/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java b/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java new file mode 100644 index 0000000000..ef28072886 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/FeatureClassLoaderLombokStubsTest.java @@ -0,0 +1,274 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import java.io.DataInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Verifies that {@link FeatureClassLoader} resolves the bundled lombok stubs + * for the class families that ECJ references when lombok is active as a JVM + * agent, and that unknown {@code lombok.*} classes are synthesised on the fly. + * + *

The tests instantiate a {@code FeatureClassLoader} with an empty URL array + * (no feature JARs) and confirm that each stubbed class can be loaded and has + * the expected members so that ECJ's static initialisers can complete without + * a {@link NoClassDefFoundError} or {@link NoSuchMethodError}. + */ +class FeatureClassLoaderLombokStubsTest { + + private FeatureClassLoader loader; + + @BeforeEach + void setUp() { + // Empty URL array โ€“ we only need the stubs that are bundled in the lib jar itself. + loader = new FeatureClassLoader(new URL[0], FeatureClassLoaderLombokStubsTest.class.getClassLoader()); + } + + // ------------------------------------------------------------------------- + // lombok.core.FieldAugment + // ------------------------------------------------------------------------- + + @Test + void fieldAugment_canBeLoaded() throws ClassNotFoundException { + Class clazz = loader.loadClass("lombok.core.FieldAugment"); + assertThat(clazz).isNotNull(); + } + + @Test + void fieldAugment_augmentMethodExists() throws Exception { + Class clazz = loader.loadClass("lombok.core.FieldAugment"); + Method augment = clazz.getMethod("augment", Class.class, Class.class, String.class); + assertThat(Modifier.isStatic(augment.getModifiers())).isTrue(); + // Must return a non-null instance so callers can safely invoke .get() etc. + assertThat(augment.invoke(null, String.class, String.class, "x")).isNotNull(); + } + + // ------------------------------------------------------------------------- + // lombok.eclipse.EcjAugments + // ------------------------------------------------------------------------- + + @Test + void ecjAugments_canBeLoaded() throws ClassNotFoundException { + Class clazz = loader.loadClass("lombok.eclipse.EcjAugments"); + assertThat(clazz).isNotNull(); + } + + @Test + void ecjAugments_astNodeGeneratedByFieldExists() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.EcjAugments"); + Field field = clazz.getField("ASTNode_generatedBy"); + assertThat(Modifier.isStatic(field.getModifiers())).isTrue(); + // Must be non-null so ECJ's patched ASTConverter can call .get() on it without NPE. + assertThat(field.get(null)).isNotNull(); + } + + @Test + void ecjAugments_innerClassEclipseAugmentsExists() throws ClassNotFoundException { + assertThatNoException().isThrownBy(() -> loader.loadClass("lombok.eclipse.EcjAugments$EclipseAugments")); + } + + // ------------------------------------------------------------------------- + // lombok.launch.PatchFixesHider and inner classes + // ------------------------------------------------------------------------- + + @Test + void patchFixesHider_moduleClassLoadingCanBeLoaded() throws ClassNotFoundException { + Class clazz = loader.loadClass("lombok.launch.PatchFixesHider$ModuleClassLoading"); + assertThat(clazz).isNotNull(); + } + + @Test + void patchFixesHider_moduleClassLoading_parserClinitIsCallable() throws Exception { + Class clazz = loader.loadClass("lombok.launch.PatchFixesHider$ModuleClassLoading"); + Method m = clazz.getMethod("parserClinit"); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + // Must not throw โ€“ this is what ECJ's Parser. calls. + assertThatNoException().isThrownBy(() -> m.invoke(null)); + } + + @Test + void patchFixesHider_transformCanBeLoaded() throws ClassNotFoundException { + assertThatNoException().isThrownBy(() -> loader.loadClass("lombok.launch.PatchFixesHider$Transform")); + } + + @Test + void patchFixesHider_patchFixes_isGeneratedOverloadsExist() throws Exception { + // We cannot use getDeclaredMethods() / getParameterTypes() here because the + // test JVM would try to resolve ECJ types (IMember, ASTNode, etc.) that are + // not on the test classpath, causing NoClassDefFoundError. + // + // Instead we read the raw .class bytes and parse the constant pool directly. + // The lombokStubs output directory is on the test classpath, so the resource + // is reachable via the test classloader โ€” no FeatureClassLoader indirection needed. + String resourcePath = "lombok/launch/PatchFixesHider$PatchFixes.class"; + URL classUrl = FeatureClassLoaderLombokStubsTest.class.getClassLoader().getResource(resourcePath); + assertThat(classUrl).as("PatchFixesHider$PatchFixes.class on test classpath").isNotNull(); + + List utf8Entries = readConstantPoolUtf8Entries(classUrl); + + // The method name "isGenerated" must appear (once, shared by all overloads). + assertThat(utf8Entries).contains("isGenerated"); + + // The two critical ECJ method descriptors that lombok transplants into ECJ bytecode + // must be present so the JVM can link the invokestatic call sites at runtime. + // (The constant pool stores full descriptors, not bare internal class names, when + // a type only appears in method signatures rather than in new/checkcast instructions.) + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;)Z"); + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/core/dom/ASTNode;)Z"); + assertThat(utf8Entries).contains("(Lorg/eclipse/jdt/core/IMember;)Z"); + } + + /** + * Reads all {@code CONSTANT_Utf8} entries from a class file's constant pool. + * Parses only the constant pool (stops before the class declaration), so it + * works on any valid class file without needing any external library. + */ + private static List readConstantPoolUtf8Entries(URL classUrl) throws Exception { + List entries = new ArrayList<>(); + try (InputStream raw = classUrl.openStream(); + DataInputStream in = new DataInputStream(raw)) { + // magic (4) + minor_version (2) + major_version (2) + in.skipBytes(8); + int cpCount = in.readUnsignedShort(); // constant_pool_count + for (int i = 1; i < cpCount; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: // CONSTANT_Utf8 + entries.add(in.readUTF()); + break; + case 3: // CONSTANT_Integer + case 4: // CONSTANT_Float + in.skipBytes(4); + break; + case 5: // CONSTANT_Long + case 6: // CONSTANT_Double + in.skipBytes(8); + i++; // long/double take two slots + break; + case 7: // CONSTANT_Class + case 8: // CONSTANT_String + case 16: // CONSTANT_MethodType + case 19: // CONSTANT_Module + case 20: // CONSTANT_Package + in.skipBytes(2); + break; + case 9: // CONSTANT_Fieldref + case 10: // CONSTANT_Methodref + case 11: // CONSTANT_InterfaceMethodref + case 12: // CONSTANT_NameAndType + case 17: // CONSTANT_Dynamic + case 18: // CONSTANT_InvokeDynamic + in.skipBytes(4); + break; + case 15: // CONSTANT_MethodHandle + in.skipBytes(3); + break; + default: + throw new IllegalStateException("Unknown constant pool tag " + tag + " at index " + i); + } + } + } + return entries; + } + + // ------------------------------------------------------------------------- + // lombok.eclipse.agent stubs + // ------------------------------------------------------------------------- + + @Test + void patchDiagnostics_setSourceRangeCheckIsCallable() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDiagnostics"); + Method m = clazz.getMethod("setSourceRangeCheck", Object.class, int.class, int.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + // Must return false so ECJ applies its normal source-range logic. + assertThat(m.invoke(null, new Object(), 0, 10)).isEqualTo(false); + } + + @Test + void patchValEclipsePortal_copyInitializationMethodsExist() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchValEclipsePortal"); + Method m = clazz.getMethod("copyInitializationOfForEachIterable", Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThatNoException().isThrownBy(() -> m.invoke(null, new Object())); + } + + @Test + void patchDelegatePortal_handleDelegateForTypeReturnsFalse() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchDelegatePortal"); + Method m = clazz.getMethod("handleDelegateForType", Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, new Object())).isEqualTo(false); + } + + @Test + void patchFixesShadowLoaded_addLombokNotesReturnsOriginal() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchFixesShadowLoaded"); + Method m = clazz.getMethod("addLombokNotesToEclipseAboutDialog", String.class, String.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, "original", "key")).isEqualTo("original"); + } + + @Test + void patchJavadoc_getHTMLContentReturnsOriginal() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.PatchJavadoc"); + Method m = clazz.getMethod("getHTMLContentFromSource", Object.class, String.class, Object.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, null, "original", null)).isEqualTo("original"); + } + + @Test + void eclipseLoaderPatcherTransplants_overrideLoadDecideReturnsFalse() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipseLoaderPatcherTransplants"); + Method m = clazz.getMethod("overrideLoadDecide", ClassLoader.class, String.class, boolean.class); + assertThat(Modifier.isStatic(m.getModifiers())).isTrue(); + assertThat(m.invoke(null, getClass().getClassLoader(), "some.Class", false)).isEqualTo(false); + } + + // ------------------------------------------------------------------------- + // synthesiseEmptyClass fallback (no bundled stub available) + // ------------------------------------------------------------------------- + + @Test + void unknownLombokClass_isSynthesisedAsEmptyClass() throws ClassNotFoundException { + // EclipsePatcher has no hand-written stub; the loader must synthesise one. + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipsePatcher"); + assertThat(clazz).isNotNull(); + assertThat(clazz.getName()).isEqualTo("lombok.eclipse.agent.EclipsePatcher"); + // Synthesised class extends Object and declares no methods beyond . + assertThat(clazz.getSuperclass()).isEqualTo(Object.class); + assertThat(clazz.getDeclaredMethods()).isEmpty(); + } + + @Test + void unknownLombokClass_canBeInstantiated() throws Exception { + Class clazz = loader.loadClass("lombok.eclipse.agent.EclipsePatcher"); + // The synthesised class has a default public ; instantiation must succeed. + assertThatNoException().isThrownBy(() -> clazz.getDeclaredConstructor().newInstance()); + } +} diff --git a/lib/src/test/java/com/diffplug/spotless/npm/NodeServerLayoutTest.java b/lib/src/test/java/com/diffplug/spotless/npm/NodeServerLayoutTest.java index 229435eb21..4a09f54f8b 100644 --- a/lib/src/test/java/com/diffplug/spotless/npm/NodeServerLayoutTest.java +++ b/lib/src/test/java/com/diffplug/spotless/npm/NodeServerLayoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ void itCalculatesDifferentNodeModulesDirForDifferentServeJs() throws IOException } static String prettierPackageJson(Map dependencies) { - String templateContent = NpmResourceHelper.readUtf8StringFromClasspath(NodeServerLayoutTest.class, "/com/diffplug/spotless/npm/prettier-package.json"); + String templateContent = NpmResourceHelper.readUtf8StringFromClasspath(NodeServerLayoutTest.class, "/com/diffplug/spotless/npm/prettier-package.json.template"); String dependenciesList = dependencies.entrySet().stream() .map(entry -> "\"%s\": \"%s\"".formatted(entry.getKey(), entry.getValue())) .reduce((a, b) -> a + ",\n " + b) diff --git a/lib/src/test/java/com/diffplug/spotless/npm/StandardNpmProcessFactoryTest.java b/lib/src/test/java/com/diffplug/spotless/npm/StandardNpmProcessFactoryTest.java new file mode 100644 index 0000000000..2a114a9774 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/npm/StandardNpmProcessFactoryTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.npm; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class StandardNpmProcessFactoryTest { + + @Test + void npmServeCommandLineOmitsRemovedScriptsPrependNodePathFlag(@TempDir Path tmp) { + File npm = tmp.resolve("npm").toFile(); + File node = tmp.resolve("node").toFile(); + File project = tmp.resolve("project").toFile(); + File build = tmp.resolve("build").toFile(); + project.mkdirs(); + build.mkdirs(); + + NpmFormatterStepLocations locations = new NpmFormatterStepLocations( + project, + build, + null, + new NpmPathResolver(npm, node, null, List.of())); + NodeServerLayout layout = new NodeServerLayout(build, "{\"name\":\"spotless-prettier\"}", "console.log('hi');"); + UUID serverId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + + NpmLongRunningProcess process = StandardNpmProcessFactory.INSTANCE.createNpmServeProcess(layout, locations, serverId); + + assertThat(process.describe()) + .contains("start") + .contains("--node-server-instance-id=" + serverId) + .doesNotContain("scripts-prepend-node-path"); + } +} diff --git a/lib/src/test/java/com/diffplug/spotless/pom/SortPomCfgTest.java b/lib/src/test/java/com/diffplug/spotless/pom/SortPomCfgTest.java index b0a336f189..aad1af2770 100644 --- a/lib/src/test/java/com/diffplug/spotless/pom/SortPomCfgTest.java +++ b/lib/src/test/java/com/diffplug/spotless/pom/SortPomCfgTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ void testDefaultValues() { SortPomCfg cfg = new SortPomCfg(); // Test default values using AssertJ - assertThat(cfg.version).isEqualTo("4.0.0"); + assertThat(cfg.version).isEqualTo(PomBuildConfig.VERSION_SORTPOM); assertThat(cfg.encoding).isEqualTo("UTF-8"); assertThat(cfg.lineSeparator).isEqualTo(System.getProperty("line.separator")); assertThat(cfg.expandEmptyElements).isFalse(); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 9665b4068c..863a487d8a 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -4,6 +4,85 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +## [8.10.4] - 2026-10-08 + +### Changes +- Bump default `eclipse` version from `4.40` to `4.41`. +- spotlessInstallGitPrePushHook now respects Gitโ€™s core.hooksPath when installing the pre-push hook, instead of always writing to .git/hooks. ([#3115](https://github.com/diffplug/spotless/pull/3115) +- Bump default `gherkin` version `42.0.0` -> `42.0.1`. ([#3088](https://github.com/diffplug/spotless/pull/3088)) +- Bump default `jackson` version `2.22.2` -> `2.22.3`. ([#3101](https://github.com/diffplug/spotless/pull/3101)) +- Bump default `palantir-java-format` version `2.98.0` -> `2.102.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103), [#3132](https://github.com/diffplug/spotless/pull/3132)) +- Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) +- Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. +- Bump default `palantir-java-format` version `2.99.0` -> `2.102.0`. ([#3132](https://github.com/diffplug/spotless/pull/3132)) + +### Fixed +- `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) + +## [8.10.3] - 2026-09-25 +### Changes +- Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `gson` version `2.13.2` -> `2.14.0`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `zjsonpatch` version `0.4.14` -> `0.4.16`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `jackson-dataformat-yaml` version `2.14.1` -> `2.20.1`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `ktfmt` version `0.63` -> `0.64`. ([2988](https://github.com/diffplug/spotless/pull/2988)) +- Bump default `cleanthat` version `2.25` -> `2.26`. ([#2882](https://github.com/diffplug/spotless/pull/2882)) +- Bump default `jackson` version `2.20.1` -> `2.22.2`. ([#2819](https://github.com/diffplug/spotless/pull/2819)) +- Bump default `javaparser` version `3.27.1` -> `3.28.2`. ([#3065](https://github.com/diffplug/spotless/pull/3065)) +- Bump default `palantir-java-format` version `2.80.0` -> `2.98.0`. ([#3068](https://github.com/diffplug/spotless/pull/3068)) +- Bump default `scalafmt` version `3.8.1` -> `3.11.5`. ([#2173](https://github.com/diffplug/spotless/pull/2173)) +- Bump default `google-java-format` version `1.30.0` -> `1.36.1`. ([#3075](https://github.com/diffplug/spotless/pull/3075)) +- Bump default `gherkin-utils` version `10.0.0` -> `12.0.2`. ([#2979](https://github.com/diffplug/spotless/pull/2979)) +- We no longer publish a plugin marker for the legacy `com.diffplug.gradle.spotless` id, which has been redirecting to `com.diffplug.spotless` since 4.0. Builds that still request it now fail with `Plugin [id: 'com.diffplug.gradle.spotless'] was not found` instead of the migration message. ([#3086](https://github.com/diffplug/spotless/pull/3086)) + +### Fixed +- Fix release signing by using Gradle's required eight-digit signing subkey ID. ([#3105](https://github.com/diffplug/spotless/pull/3105)) +- Fix race when creating the npm install cache directory. (([#3096](https://github.com/diffplug/spotless/pull/3096)) +- GrEclipse no longer emits expected OSGi and nested-jar warnings during initialization. ([#2445](https://github.com/diffplug/spotless/issues/2445)) +- `typescript` `prettier()` no longer emits a warning when its parser is already set to `typescript`. ([#3098](https://github.com/diffplug/spotless/pull/3098)) +- `versionCatalog()` preserves standalone comments at section boundaries and the end of the file. ([#3048](https://github.com/diffplug/spotless/issues/3048)) +- `versionCatalog()` preserves entries when comments contain unmatched brackets, preserves commas inside quoted strings, and keeps significant line boundaries in multiline entries. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- `versionCatalog()` now reports unfinished entries as lints at their starting line. These fail formatting by default, so upgrading may expose catalog errors that previously caused silent data loss. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- Stop calling deprecated `Configuration.setVisible` from Gradle 9.0.0 ([#3053](https://github.com/diffplug/spotless/pull/3053)) +- Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` or `NoSuchMethodError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [8.10.2] - 2026-09-04 +### Fixed +- `shortenFullyQualifiedTypes()` now shortens fully-qualified types used in expression contexts (such as static method calls, static fields, and enum constants) while avoiding imports that would change how existing unqualified type references resolve. ([#3039](https://github.com/diffplug/spotless/pull/3039)) +- Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [8.10.1] - 2026-08-27 +### Fixed +- `prettier()` and other npm-based steps no longer fail to start on npm 12 (`EUNKNOWNCONFIG` from `--scripts-prepend-node-path`). ([#3024](https://github.com/diffplug/spotless/issues/3024)) +- `spotlessInternalRegisterDependencies` now writes its output under a build directory that is configured after the plugin is applied, instead of always under the default `build/`. ([#2114](https://github.com/diffplug/spotless/issues/2114)) +- `targetExclude` now accepts a Gradle `Directory`, `DirectoryProperty`, or `Provider` and excludes the files under it. Previously the directory was treated as a single file, so excluding one silently did nothing. ([#2667](https://github.com/diffplug/spotless/issues/2667)) + +## [8.10.0] - 2026-08-17 +### Added +- New `shortenFullyQualifiedTypes()` step for Java, which replaces fully-qualified type names with their simple names and adds the imports they need. Best combined with `importOrder()` and `removeUnusedImports()`. ([#2945](https://github.com/diffplug/spotless/issues/2945)) +- Add embedded lockfiles to Eclipse JDT for every supported version (`4.9` through `4.40`), so `eclipse()` resolves from Maven Central instead of querying a P2 update site. Versions without an embedded lockfile still fall back to P2 provisioning. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +### Fixed +- `removeUnusedImports` no longer fails on Java `import module` declarations. ([#2890](https://github.com/diffplug/spotless/issues/2890)) +- `expandWildcardImports()` now builds its type-solver classpath from each Java source set's compile classpath instead of every resolvable configuration. Unrelated configurations (for example generated-code or custom resolvable configs that are not ready yet) are no longer resolved. ([#2998](https://github.com/diffplug/spotless/issues/2998)) +- `spotlessCheck` violation message now suggests the correct composite/included-build task path (e.g. `./gradlew :my-utils:spotlessApply`) instead of a bare `spotlessApply` / `:spotlessApply` that does not select included-build tasks. ([#2421](https://github.com/diffplug/spotless/issues/2421)) +- Parallel multi-project builds no longer intermittently fail with "Cannot fingerprint input property 'stepsInternalEquality': ConfigurationCacheHackList cannot be serialized" / "Failed to provision P2 dependencies" when using `eclipse()` (or other P2-backed steps). Subprojects now share one deduping P2 provisioner and P2 queries are serialized process-wide. ([#3004](https://github.com/diffplug/spotless/issues/3004)) +### Changes +- Default `google-java-format` remains `1.28.0` on JVM 17; bumps to `1.30.0` on JVM 21+; require at least `1.30.0` on JVM 25+ for `import module` support. +- Bump default `eclipse` version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +- Bump default `adocfmt` version `0.2.0` -> `0.3.1`, which adds table formatting support (`formatTables`, `tableLayout`, `tableMaxLineWidth`, `tableBlankLines`). + +## [8.9.0] - 2026-07-27 +### Added +- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space) with the new `princeOfSpace()` step. ([#2991](https://github.com/diffplug/spotless/pull/2991)) +### Fixed +- Prevent parallel Gradle input fingerprinting from failing when `toggleOffOn()` wraps a slow lazy formatter step with no matching target files. ([#2994](https://github.com/diffplug/spotless/pull/2994)) +### Changes +- Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- Bump default `tabletest-formatter` version `1.1.1` -> `1.1.2`. + ## [8.8.0] - 2026-06-29 ### Added - Add support for custom string format for license header copyright year via `yearStringFormat()`. ([#2965](https://github.com/diffplug/spotless/pull/2965)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 443acf7851..258fd6450b 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -10,19 +10,42 @@ output = [ '', link(shield('VS Code plugin', 'IDE', 'VS Code', 'blueviolet'), 'https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle'), link(shield('IntelliJ plugin', 'IDE', 'IntelliJ', 'blueviolet'), 'https://plugins.jetbrains.com/plugin/18321-spotless-gradle'), + link(shield('Spotless Daemon plugin', 'IDE', 'Spotless Daemon', 'blueviolet'), 'https://plugins.jetbrains.com/plugin/29287-spotless-daemon'), link(shield('Add other IDE', 'IDE', 'add yours', 'blueviolet'), 'IDE_HOOK.md') ].join('\n'); --> [![Gradle plugin](https://img.shields.io/badge/plugins.gradle.org-com.diffplug.spotless-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.spotless) -[![Changelog](https://img.shields.io/badge/changelog-8.8.0-blue.svg)](CHANGES.md) +[![Changelog](https://img.shields.io/badge/changelog-8.10.4-blue.svg)](CHANGES.md) [![MavenCentral](https://img.shields.io/badge/mavencentral-here-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22) -[![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/index.html) +[![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/index.html) [![VS Code plugin](https://img.shields.io/badge/IDE-VS_Code-blueviolet.svg)](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) [![IntelliJ plugin](https://img.shields.io/badge/IDE-IntelliJ-blueviolet.svg)](https://plugins.jetbrains.com/plugin/18321-spotless-gradle) +[![Spotless Daemon plugin](https://img.shields.io/badge/IDE-Spotless_Daemon-blueviolet.svg)](https://plugins.jetbrains.com/plugin/29287-spotless-daemon) [![Add other IDE](https://img.shields.io/badge/IDE-add_yours-blueviolet.svg)](IDE_HOOK.md) +Two IntelliJ plugins are available, with different trade-offs: + +**[Spotless Daemon](https://plugins.jetbrains.com/plugin/29287-spotless-daemon)** + +| | | +|:---|:---------------------------------------------------------------------------------------------------------| +| โœ… | Fast response โ€” the Gradle daemon stays resident, so each format skips Gradle's startup and task execution | +| โœ… | Replaces the IDE's built-in formatter, so the formatting UX is identical to native reformat | +| โœ… | Includes targeted optimizations, e.g. import optimization is preprocessed on the IDE side | +| โŒ | The resident daemon holds some resources while running | +| โŒ | Implementation depends on some internal Gradle plugin structures rather than public integration APIs | + +**[Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle)** + +| | | +|:---|:---------------------------------------------------------------------------------------------------------| +| โœ… | Stable and battle-tested over years of use | +| โœ… | Works through the standard `spotlessApply` Gradle task integration | +| โŒ | Slow cold start โ€” every invocation pays Gradle's startup cost | +| โŒ | Runs as a separate action, disconnected from the IDE's built-in format UX | + @@ -56,7 +79,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [Git hook (optional)](#git-hook) - [Linting](#linting) - **Languages** - - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) + - [Java](#java) ([removeUnusedImports](#removeunusedimports), [forbidWildcardImports](#forbidwildcardimports), [expandWildcardImports](#expandwildcardimports), [forbidModuleImports](#forbidmoduleimports), [shortenFullyQualifiedTypes](#shortenfullyqualifiedtypes), [google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [prince-of-space](#prince-of-space), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [tabletest-formatter](#tabletest-formatter-1), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) @@ -133,10 +156,10 @@ spotless { ``` Spotless consists of a list of formats (in the example above, `misc` and `java`), and each format has: -- a `target` (the files to format), which you set with [`target`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#target-java.lang.Object...-) and [`targetExclude`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#targetExclude-java.lang.Object...-) -- a list of `FormatterStep`, which are just `String -> String` functions, such as [`replace`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#replace-java.lang.String-java.lang.CharSequence-java.lang.CharSequence-), [`replaceRegex`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#replaceRegex-java.lang.String-java.lang.String-java.lang.String-), [`trimTrailingWhitespace`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#replace-java.lang.String-java.lang.CharSequence-java.lang.CharSequence-), [`custom`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#custom-java.lang.String-groovy.lang.Closure-), [`prettier`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#prettier--), [`eclipseWtp`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#eclipseWtp-com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep-), [`licenseHeader`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#licenseHeader-java.lang.String-java.lang.String-) etc. +- a `target` (the files to format), which you set with [`target`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#target-java.lang.Object...-) and [`targetExclude`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#targetExclude-java.lang.Object...-) +- a list of `FormatterStep`, which are just `String -> String` functions, such as [`replace`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#replace-java.lang.String-java.lang.CharSequence-java.lang.CharSequence-), [`replaceRegex`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#replaceRegex-java.lang.String-java.lang.String-java.lang.String-), [`trimTrailingWhitespace`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#replace-java.lang.String-java.lang.CharSequence-java.lang.CharSequence-), [`custom`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#custom-java.lang.String-groovy.lang.Closure-), [`prettier`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#prettier--), [`eclipseWtp`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#eclipseWtp-com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep-), [`licenseHeader`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#licenseHeader-java.lang.String-java.lang.String-) etc. -All the generic steps live in [`FormatExtension`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html), and there are many language-specific steps which live in its language-specific subclasses, which are described below. +All the generic steps live in [`FormatExtension`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html), and there are many language-specific steps which live in its language-specific subclasses, which are described below. ### Requirements @@ -193,7 +216,7 @@ Spotless is primarily a formatter, _not_ a linter. In our opinion, a linter is j ## Java -`com.diffplug.gradle.spotless.JavaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/JavaExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java) +`com.diffplug.gradle.spotless.JavaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/JavaExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java) ```gradle spotless { @@ -210,6 +233,7 @@ spotless { removeUnusedImports() forbidWildcardImports() // or expandWildcardImports, see below forbidModuleImports() + shortenFullyQualifiedTypes() // replaces fully-qualified type names with simple names + imports // Cleanthat will refactor your code, but it may break your style: apply it before your formatter cleanthat() // has its own section below @@ -265,7 +289,7 @@ spotless { ### expandWildcardImports This step expands all wildcard imports to single class imports. -To do this, [JavaParser](https://javaparser.org/) is used to parse the complete sourcecode and resolve the full qualified name of all used classes and static methods. +To do this, [JavaParser](https://javaparser.org/) is used to parse the complete sourcecode and resolve the full qualified name of all used classes and static methods from the full classpath. This operation can be resource intensive when formatting many source files, so you may want to change to `forbidWildcardImports` when your codebase is cleaned and stable. ``` @@ -286,6 +310,67 @@ spotless { } ``` +### shortenFullyQualifiedTypes + +Replaces fully-qualified type names with their simple names, adding the imports they need. Useful for cleaning up generated code, or code where inline fully-qualified names have crept in. + +[JavaParser](https://javaparser.org/) parses the source and only type positions in the AST are rewritten, so fully-qualified names appearing in strings, text blocks, and comments are left alone. Unlike [`expandWildcardImports`](#expandwildcardimports), it works from the source file alone and does not need your compile classpath. + +New imports are appended after the existing ones, so run this before `importOrder()`: + +```gradle +spotless { + java { + shortenFullyQualifiedTypes() + importOrder() + removeUnusedImports() + } +} +``` + +Before: + +```java +package com.acme; + +public class UserService { + private final java.util.Map> cache = new java.util.HashMap<>(); + + public java.util.List getUsers(java.util.function.Predicate filter) throws java.io.IOException { + return new java.util.ArrayList<>(); + } +} +``` + +After: + +```java +package com.acme; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class UserService { + private final Map> cache = new HashMap<>(); + + public List getUsers(Predicate filter) throws IOException { + return new ArrayList<>(); + } +} +``` + +A name is left fully-qualified whenever shortening it could change what the code means: + +- two different fully-qualified names in the file would collapse to the same simple name (e.g. `java.util.List` and `java.awt.List`) +- an existing import already binds that simple name to a different type +- the file does not parse + +Types in `java.lang` and in the file's own package are shortened without adding an import. + ### google-java-format [homepage](https://github.com/google/google-java-format). [changelog](https://github.com/google/google-java-format/releases). @@ -312,6 +397,24 @@ spotless { palantirJavaFormat('2.39.0').formatJavadoc(true) ``` +### prince-of-space + +[homepage](https://github.com/agustafson/prince-of-space). Requires a JDK 17+ host runtime. +```gradle +spotless { + java { + princeOfSpace() + // optional: configure any of the formatter's options + princeOfSpace('2.2.0') + .indentStyle('SPACES') // or 'TABS' + .indentSize(4) + .lineLength(120) + .wrapStyle('BALANCED') // or 'WIDE', 'NARROW' + .closingParenOnNewLine(true) + .trailingCommas(false) + .javaLanguageLevel(17) +``` + ### eclipse jdt [homepage](https://download.eclipse.org/eclipse/downloads/). See [here](../ECLIPSE_SCREENSHOTS.md) for screenshots that demonstrate how to get and install the config file mentioned below. @@ -441,15 +544,15 @@ spotless { java { tableTestFormatter() // optional: you can specify a specific version - tableTestFormatter('1.1.1') + tableTestFormatter('1.1.2') ``` ## Groovy -- `com.diffplug.gradle.spotless.GroovyExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/GroovyExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java) -- `com.diffplug.gradle.spotless.GroovyGradleExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/GroovyGradleExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java) +- `com.diffplug.gradle.spotless.GroovyExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/GroovyExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java) +- `com.diffplug.gradle.spotless.GroovyGradleExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/GroovyGradleExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java) Configuration for Groovy is similar to [Java](#java), in that it also supports `licenseHeader` and `importOrder`. @@ -512,8 +615,8 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ## Kotlin -- `com.diffplug.gradle.spotless.KotlinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/KotlinExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java) -- `com.diffplug.gradle.spotless.KotlinGradleExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/KotlinGradleExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java) +- `com.diffplug.gradle.spotless.KotlinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/KotlinExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java) +- `com.diffplug.gradle.spotless.KotlinGradleExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/KotlinGradleExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java) ```gradle @@ -546,7 +649,7 @@ spotless { // if you are using build.gradle.kts, instead of 'spotless {' use: ### ktfmt -[homepage](https://github.com/facebook/ktfmt). [changelog](https://github.com/facebook/ktfmt/releases). +[homepage](https://github.com/Kotlin/ktfmt). [changelog](https://github.com/Kotlin/ktfmt/releases). ```kotlin spotless { @@ -618,14 +721,14 @@ spotless { kotlin { tableTestFormatter() // optional: you can specify a specific version - tableTestFormatter('1.1.1') + tableTestFormatter('1.1.2') ``` ## Scala -`com.diffplug.gradle.spotless.ScalaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/ScalaExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java) +`com.diffplug.gradle.spotless.ScalaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/ScalaExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java) ```gradle spotless { @@ -657,7 +760,7 @@ spotless { ## C/C++ -`com.diffplug.gradle.spotless.CppExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/CppExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java) +`com.diffplug.gradle.spotless.CppExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/CppExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java) ```gradle spotless { @@ -697,7 +800,7 @@ spotles { ## Python -`com.diffplug.gradle.spotless.PythonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/PythonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java) +`com.diffplug.gradle.spotless.PythonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/PythonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java) ```gradle spotless { @@ -733,7 +836,7 @@ black().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe') ### buf -`com.diffplug.gradle.spotless.ProtobufExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/ProtobufExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java) +`com.diffplug.gradle.spotless.ProtobufExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/ProtobufExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java) **WARNING** this step **must** be the first step in the chain, steps before it will be ignored. Thumbs up [this issue](https://github.com/bufbuild/buf/issues/1035) for a resolution, see [here](https://github.com/diffplug/spotless/pull/1208#discussion_r1264439669) for more details on the problem. @@ -765,7 +868,7 @@ buf { ## FreshMark -`com.diffplug.gradle.spotless.FreshMarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FreshMarkExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java) +`com.diffplug.gradle.spotless.FreshMarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FreshMarkExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java) [homepage](https://github.com/diffplug/freshmark). [changelog](https://github.com/diffplug/freshmark/blob/master/CHANGES.md). FreshMark lets you generate markdown in the comments of your markdown. This helps to keep badges and links up-to-date (see the source for this file), and can also be helpful for generating complex tables (see the source for [the parent readme](../README.md)). @@ -786,7 +889,7 @@ spotless { ## Flexmark -`com.diffplug.gradle.spotless.FlexmarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FlexmarkExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java) +`com.diffplug.gradle.spotless.FlexmarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FlexmarkExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java) [homepage](https://github.com/vsch/flexmark-java). Flexmark is a flexible Commonmark/Markdown parser that can be used to format Markdown files. It supports different [flavors of Markdown](https://github.com/vsch/flexmark-java#markdown-processor-emulation) and [many formatting options](https://github.com/vsch/flexmark-java/wiki/Markdown-Formatter#options). @@ -814,7 +917,7 @@ spotless { ## Antlr4 -`com.diffplug.gradle.spotless.Antlr4Extension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/Antlr4Extension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/Antlr4Extension.java) +`com.diffplug.gradle.spotless.Antlr4Extension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/Antlr4Extension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/Antlr4Extension.java) ```gradle spotless { @@ -836,7 +939,7 @@ antlr4formatter('1.2.1') // version is optional ## Asciidoc -`com.diffplug.gradle.spotless.AsciidocExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/AsciidocExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java) +`com.diffplug.gradle.spotless.AsciidocExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/AsciidocExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java) ```gradle spotless { @@ -853,7 +956,7 @@ spotless { [homepage](https://github.com/dheid/adocfmt). [available versions](https://search.maven.org/artifact/org.drjekyll/adocfmt). ```gradle -adocfmt('0.2.0') // version is optional +adocfmt('0.3.1') // version is optional .normalizeSetextHeadings(false) // convert === underlines to ATX == (default: false) .collapseConsecutiveBlankLines(true) // max 1 blank line (default: true) .oneSentencePerLine(false) // each sentence on its own line (default: false) @@ -865,6 +968,10 @@ adocfmt('0.2.0') // version is optional .normalizeOrderedListMarkers(false) // 1. -> . (default: false) .ensureHeadingBlankLines(true) // blank line before/after headings (default: true) .ensureSourceDelimiters(false) // wrap [source] in ---- (default: false) + .formatTables(true) // format AsciiDoc tables (default: true) + .tableLayout('AUTO') // AUTO, EXPANDED, PRESERVE (default: AUTO) + .tableMaxLineWidth(120) // max line width for compact table layout (default: 120) + .tableBlankLines('ALL') // NONE, HEADER, ALL, PRESERVE (default: ALL) ``` Options default to `true` or `false` as shown above. This formatter is designed to help you maintain a clean, consistent AsciiDoc structure. Surface-altering options like `oneSentencePerLine` and `normalizeSetextHeadings` default to `false` so that zero-config formatting is conservative; opt in explicitly if you want those transforms. @@ -874,7 +981,7 @@ Options default to `true` or `false` as shown above. This formatter is designed ## SQL -`com.diffplug.gradle.spotless.SqlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/SqlExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java) +`com.diffplug.gradle.spotless.SqlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/SqlExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java) ```gradle spotless { @@ -913,7 +1020,7 @@ sql.formatter.indent.size=4 ## Maven POM -`com.diffplug.gradle.spotless.PomExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/PomExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PomExtension.java) +`com.diffplug.gradle.spotless.PomExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/PomExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PomExtension.java) ```gradle spotless { @@ -962,7 +1069,7 @@ spotless { ## Typescript -- `com.diffplug.gradle.spotless.TypescriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/TypescriptExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java) +- `com.diffplug.gradle.spotless.TypescriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/TypescriptExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java) ```gradle spotless { @@ -1056,7 +1163,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr ## Javascript -- `com.diffplug.gradle.spotless.JavascriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/JavascriptExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java) +- `com.diffplug.gradle.spotless.JavascriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/JavascriptExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java) ```gradle spotless { @@ -1119,7 +1226,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr ## JSON -- `com.diffplug.gradle.spotless.JsonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/JsonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java) +- `com.diffplug.gradle.spotless.JsonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/JsonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java) ```gradle spotless { @@ -1239,7 +1346,7 @@ spotless { ## YAML -- `com.diffplug.gradle.spotless.YamlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/YamlExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java) +- `com.diffplug.gradle.spotless.YamlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/YamlExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java) ```gradle spotless { @@ -1271,7 +1378,7 @@ spotless { ## Shell -`com.diffplug.gradle.spotless.ShellExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/ShellExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java) +`com.diffplug.gradle.spotless.ShellExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/ShellExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java) ```gradle spotless { @@ -1328,14 +1435,14 @@ spotless { target 'src/**/*.table' tableTestFormatter() // optional: you can specify a specific version - tableTestFormatter('1.1.1') + tableTestFormatter('1.1.2') } } ``` ## Gherkin -- `com.diffplug.gradle.spotless.GherkinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/GherkinExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java) +- `com.diffplug.gradle.spotless.GherkinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/GherkinExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java) ```gradle spotless { @@ -1364,7 +1471,7 @@ spotless { ## CSS -`com.diffplug.gradle.spotless.CssExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/CssExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CssExtension.java) +`com.diffplug.gradle.spotless.CssExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/CssExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CssExtension.java) ```gradle spotless { @@ -1820,7 +1927,7 @@ Once a file's license header has a valid year, whether it is a year (`2020`) or * `2017` -> `2017-2020` * `2017-2019` -> `2017-2020` -See the [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.LicenseHeaderConfig.html) for a complete listing of options. +See the [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.LicenseHeaderConfig.html) for a complete listing of options. @@ -1906,9 +2013,9 @@ spotless { custom 'lowercase', { str -> str.toLowerCase() } ``` -However, custom rules will disable up-to-date checking and caching, unless you read [this javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#bumpThisNumberIfACustomStepChanges-int-) and follow its instructions carefully. +However, custom rules will disable up-to-date checking and caching, unless you read [this javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#bumpThisNumberIfACustomStepChanges-int-) and follow its instructions carefully. -Another option is to create proper `FormatterStep` in your `buildSrc`, and then call [`addStep`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#addStep-com.diffplug.spotless.FormatterStep-). The contributing guide describes [how to do this](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#how-to-add-a-new-formatterstep). If the step is generally-useful, we hope you'll open a PR to share it! +Another option is to create proper `FormatterStep` in your `buildSrc`, and then call [`addStep`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#addStep-com.diffplug.spotless.FormatterStep-). The contributing guide describes [how to do this](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#how-to-add-a-new-formatterstep). If the step is generally-useful, we hope you'll open a PR to share it! ```gradle @@ -1941,11 +2048,11 @@ spotless { format 'foo', com.acme.FooLanguageExtension, { ``` -If you'd like to create a one-off Spotless task outside of the `check`/`apply` framework, see [`FormatExtension.createIndependentApplyTask`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#createIndependentApplyTask-java.lang.String-). +If you'd like to create a one-off Spotless task outside of the `check`/`apply` framework, see [`FormatExtension.createIndependentApplyTask`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#createIndependentApplyTask-java.lang.String-). ## Inception (languages within languages within...) -In very rare cases, you might want to format e.g. javascript which is written inside JSP templates, or maybe java within a markdown file, or something wacky like that. You can specify hunks within a file using either open/close tags or a regex with a single capturing group, and then specify rules within it, like so. See [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.8.0/com/diffplug/gradle/spotless/FormatExtension.html#withinBlocks-java.lang.String-java.lang.String-java.lang.String-org.gradle.api.Action-) for more details. +In very rare cases, you might want to format e.g. javascript which is written inside JSP templates, or maybe java within a markdown file, or something wacky like that. You can specify hunks within a file using either open/close tags or a regex with a single capturing group, and then specify rules within it, like so. See [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/8.10.4/com/diffplug/gradle/spotless/FormatExtension.html#withinBlocks-java.lang.String-java.lang.String-java.lang.String-org.gradle.api.Action-) for more details. ```gradle import com.diffplug.gradle.spotless.JavaExtension diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle deleted file mode 100644 index 8f79641d41..0000000000 --- a/plugin-gradle/build.gradle +++ /dev/null @@ -1,94 +0,0 @@ -apply from: rootProject.file('gradle/changelog.gradle') -ext.artifactId = project.artifactIdGradle -version = spotlessChangelog.versionNext -apply plugin: 'java-library' -apply plugin: 'com.gradle.plugin-publish' -apply plugin: 'java-gradle-plugin' -apply from: rootProject.file('gradle/java-setup.gradle') -apply from: rootProject.file('gradle/spotless-freshmark.gradle') - - -dependencies { - if (version.endsWith('-SNAPSHOT') || (rootProject.spotlessChangelog.versionNext == rootProject.spotlessChangelog.versionLast)) { - api projects.lib - api projects.libExtra - } else { - api "com.diffplug.spotless:spotless-lib:${rootProject.spotlessChangelog.versionLast}" - api "com.diffplug.spotless:spotless-lib-extra:${rootProject.spotlessChangelog.versionLast}" - } - implementation libs.durian.core - implementation libs.durian.io - implementation libs.durian.collect - implementation libs.jgit - - testImplementation projects.testlib - testImplementation libs.junit.jupiter - testImplementation libs.assertj.core - testImplementation libs.durian.testlib - testImplementation libs.owasp.encoder - testImplementation libs.solstice - testRuntimeOnly "org.junit.platform:junit-platform-launcher" -} - -apply from: rootProject.file('gradle/special-tests.gradle') -tasks.withType(Test).configureEach { - testLogging.showStandardStreams = true -} -tasks.validatePlugins { - // TODO: https://github.com/gradle/gradle/issues/22600 - enableStricterValidation = true -} - -////////////////////////// -// GRADLE PLUGIN PORTAL // -////////////////////////// -gradlePlugin { - website = "https://github.com/diffplug/spotless" - vcsUrl = "https://github.com/diffplug/spotless" - plugins { - spotlessPlugin { - id = 'com.diffplug.spotless' - implementationClass = 'com.diffplug.gradle.spotless.SpotlessPlugin' - displayName = 'Spotless formatting plugin' - description = project.description - tags.set([ - 'format', - 'style', - 'license', - 'header', - 'google-java-format', - 'eclipse', - 'ktlint', - 'ktfmt', - 'diktat', - 'tsfmt', - 'prettier', - 'scalafmt', - 'scalafix', - 'black', - 'clang-format' - ]) - compatibility { - features { - configurationCache = true - } - } - } - spotlessPluginLegacy { - id = 'com.diffplug.gradle.spotless' - implementationClass = 'com.diffplug.gradle.spotless.SpotlessPluginRedirect' - displayName = 'Spotless formatting plugin (legacy)' - description = project.description - tags.set([ - 'format' - ]) - } - } -} - -tasks.named("publishPlugins") { - enabled = !version.endsWith('-SNAPSHOT') -} - -// have to apply java-publish after setting up the pluginBundle -apply from: rootProject.file('gradle/java-publish.gradle') diff --git a/plugin-gradle/build.gradle.kts b/plugin-gradle/build.gradle.kts new file mode 100644 index 0000000000..593990399e --- /dev/null +++ b/plugin-gradle/build.gradle.kts @@ -0,0 +1,104 @@ +import org.gradle.api.plugins.JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.plugins.JavaPlugin.JAVADOC_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.plugins.JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME +import org.gradle.plugin.compatibility.compatibility + +plugins { + `java-library` + `java-gradle-plugin` + alias(libs.plugins.plugin.publish) + id("spotless.changelog") + id("spotless.java-setup") + id("spotless.spotless-freshmark") + id("spotless.special-tests") + // must come after java-gradle-plugin, which is what creates the 'pluginMaven' publication that + // spotless.java-publish then configures + id("spotless.java-publish") +} + +version = spotlessChangelog.versionNext + +val publishedElements = + listOf( + API_ELEMENTS_CONFIGURATION_NAME, + RUNTIME_ELEMENTS_CONFIGURATION_NAME, + JAVADOC_ELEMENTS_CONFIGURATION_NAME, + SOURCES_ELEMENTS_CONFIGURATION_NAME, + ) + +configurations.configureEach { + when (name) { + in publishedElements -> + outgoing { + // Main/current capability. + capability("com.diffplug.spotless:spotless-plugin-gradle:$version") + // Historical capabilities. + capability("com.diffplug.gradle.spotless:spotless-plugin-gradle:$version") + } + } +} + +dependencies { + if ( + version.toString().endsWith("-SNAPSHOT") || + (rootSpotlessChangelog.versionNext == rootSpotlessChangelog.versionLast) + ) { + api(projects.lib) + api(projects.libExtra) + } else { + api("com.diffplug.spotless:spotless-lib:${rootSpotlessChangelog.versionLast}") + api("com.diffplug.spotless:spotless-lib-extra:${rootSpotlessChangelog.versionLast}") + } + implementation(libs.durian.core) + implementation(libs.durian.io) + implementation(libs.durian.collect) + implementation(libs.jgit) + + testImplementation(projects.testlib) + testImplementation(libs.junit.jupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.durian.testlib) + testImplementation(libs.owasp.encoder) + testImplementation(libs.solstice) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType().configureEach { testLogging.showStandardStreams = true } + +////////////////////////// +// GRADLE PLUGIN PORTAL // +////////////////////////// +gradlePlugin { + website = "https://github.com/diffplug/spotless" + vcsUrl = "https://github.com/diffplug/spotless" + plugins { + create("spotlessPlugin") { + id = "com.diffplug.spotless" + implementationClass = "com.diffplug.gradle.spotless.SpotlessPlugin" + displayName = "Spotless formatting plugin" + description = property("POM_DESCRIPTION").toString() + tags = + listOf( + "format", + "style", + "license", + "header", + "google-java-format", + "eclipse", + "ktlint", + "ktfmt", + "diktat", + "tsfmt", + "prettier", + "scalafmt", + "scalafix", + "black", + "clang-format", + ) + compatibility { features { configurationCache = true } } + } + } +} + +tasks.publishPlugins { enabled = !version.toString().endsWith("-SNAPSHOT") } diff --git a/plugin-gradle/gradle.properties b/plugin-gradle/gradle.properties new file mode 100644 index 0000000000..e01cb97a59 --- /dev/null +++ b/plugin-gradle/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=spotless-plugin-gradle +POM_NAME=spotless-plugin-gradle diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java index cf8f3c883f..1f97815fd4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AsciidocExtension.java @@ -110,6 +110,30 @@ public AdocfmtFormatterConfig ensureSourceDelimiters(boolean ensureSourceDelimit replaceStep(AdocfmtStep.create(version, provisioner(), config)); return this; } + + public AdocfmtFormatterConfig formatTables(boolean formatTables) { + config.formatTables = formatTables; + replaceStep(AdocfmtStep.create(version, provisioner(), config)); + return this; + } + + public AdocfmtFormatterConfig tableLayout(String tableLayout) { + config.tableLayout = tableLayout; + replaceStep(AdocfmtStep.create(version, provisioner(), config)); + return this; + } + + public AdocfmtFormatterConfig tableMaxLineWidth(int tableMaxLineWidth) { + config.tableMaxLineWidth = tableMaxLineWidth; + replaceStep(AdocfmtStep.create(version, provisioner(), config)); + return this; + } + + public AdocfmtFormatterConfig tableBlankLines(String tableBlankLines) { + config.tableBlankLines = tableBlankLines; + replaceStep(AdocfmtStep.create(version, provisioner(), config)); + return this; + } } @Override diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index c3dc731215..2c99624d00 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -57,13 +57,13 @@ public KtlintConfig ktlint(String version) throws IOException { return new KtlintConfig(version, Collections.emptyMap(), Collections.emptyList()); } - /** Uses the ktfmt jar to format source code. */ + /** Uses the ktfmt jar to format source code. */ public KtfmtConfig ktfmt() { return ktfmt(KtfmtStep.defaultVersion()); } /** - * Uses the given version of ktfmt and applies the dropbox style + * Uses the given version of ktfmt and applies the dropbox style * option to format source code. */ public KtfmtConfig ktfmt(String version) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 48f3e50eb9..e1cc198067 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -47,7 +47,9 @@ import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.Directory; import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemLocation; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; import org.gradle.util.GradleVersion; import org.slf4j.Logger; @@ -344,6 +346,9 @@ private final FileCollection parseTargetIsExclude(Object target, boolean isExclu matchedFiles.exclude(excludes); } return matchedFiles; + } else if (isExclude && (target instanceof FileSystemLocation || target instanceof Provider)) { + // an excluded directory means the files under it, and the tree resolves them lazily + return getProject().files(target).getAsFileTree(); } else { return getProject().files(target); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java index 52196faf83..1eccba6f6e 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java @@ -35,6 +35,7 @@ import org.gradle.api.attributes.Category; import org.gradle.api.attributes.java.TargetJvmEnvironment; import org.gradle.api.initialization.dsl.ScriptHandler; +import org.gradle.util.GradleVersion; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,16 +54,16 @@ enum Policy { public DedupingProvisioner dedupingProvisioner(Project project) { return switch (this) { - case ROOT_PROJECT -> new DedupingProvisioner(forProject(project)); - case ROOT_BUILDSCRIPT -> new DedupingProvisioner(forRootProjectBuildscript(project)); - default -> throw Unhandled.enumException(this); + case ROOT_PROJECT -> new DedupingProvisioner(forProject(project)); + case ROOT_BUILDSCRIPT -> new DedupingProvisioner(forRootProjectBuildscript(project)); + default -> throw Unhandled.enumException(this); }; } public DedupingP2Provisioner dedupingP2Provisioner(Project project) { return switch (this) { - case ROOT_PROJECT, ROOT_BUILDSCRIPT -> new DedupingP2Provisioner(P2Provisioner.createDefault(), defaultP2CacheDirectory(project)); - default -> throw Unhandled.enumException(this); + case ROOT_PROJECT, ROOT_BUILDSCRIPT -> new DedupingP2Provisioner(P2Provisioner.createDefault(), defaultP2CacheDirectory(project)); + default -> throw Unhandled.enumException(this); }; } } @@ -133,7 +134,10 @@ private static Provisioner forConfigurationContainer(Project project, Configurat config.setDescription("Spotless internal dependency resolution for " + request); config.setTransitive(withTransitives); config.setCanBeConsumed(false); - config.setVisible(false); + if (GradleVersion.current().compareTo(GradleVersion.version("9.0.0")) < 0) { + // Deprecated from 9.1.0 + config.setVisible(false); + } config.attributes(attr -> { attr.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY)); attr.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL)); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java index 4ea13c261d..9411811322 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java @@ -31,7 +31,6 @@ import javax.inject.Inject; import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -46,7 +45,9 @@ import com.diffplug.spotless.java.GoogleJavaFormatStep; import com.diffplug.spotless.java.ImportOrderStep; import com.diffplug.spotless.java.PalantirJavaFormatStep; +import com.diffplug.spotless.java.PrinceOfSpaceStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; +import com.diffplug.spotless.java.ShortenFullyQualifiedTypesStep; import com.diffplug.spotless.java.TableTestFormatterStep; public class JavaExtension extends FormatExtension implements HasBuiltinDelimiterForLicense, JvmLang { @@ -168,6 +169,11 @@ public void forbidWildcardImports() { addStep(ForbidWildcardImportsStep.create()); } + /** Shortens fully qualified type names and adds imports. */ + public void shortenFullyQualifiedTypes() { + addStep(ShortenFullyQualifiedTypesStep.create(provisioner())); + } + public void forbidModuleImports() { addStep(ForbidModuleImportsStep.create()); } @@ -175,7 +181,10 @@ public void forbidModuleImports() { public void expandWildcardImports() { SourceSetContainer sourceSets = getSourceSets(getProject(), "expansion of wildcards requires the 'java' plugin to be applied"); Set typeSolverClasspath = sourceSets.stream().flatMap(s -> s.getAllJava().getSrcDirs().stream()).collect(toSet()); - getProject().getConfigurations().stream().filter(Configuration::isCanBeResolved).flatMap(c -> c.getFiles().stream()).forEach(typeSolverClasspath::add); + sourceSets.stream() + .map(SourceSet::getCompileClasspath) + .flatMap(classpath -> classpath.getFiles().stream()) + .forEach(typeSolverClasspath::add); addStep(ExpandWildcardImportsStep.create(typeSolverClasspath, provisioner())); } @@ -308,6 +317,73 @@ private FormatterStep createStep() { } } + /** Uses the prince-of-space jar to format source code. */ + public PrinceOfSpaceConfig princeOfSpace() { + return princeOfSpace(PrinceOfSpaceStep.defaultVersion()); + } + + /** Uses the given version of prince-of-space to format source code. */ + public PrinceOfSpaceConfig princeOfSpace(String version) { + Objects.requireNonNull(version); + return new PrinceOfSpaceConfig(version); + } + + public class PrinceOfSpaceConfig { + final String version; + final PrinceOfSpaceStep.Options options = new PrinceOfSpaceStep.Options(); + + PrinceOfSpaceConfig(String version) { + this.version = Objects.requireNonNull(version); + addStep(createStep()); + } + + public PrinceOfSpaceConfig indentStyle(String indentStyle) { + options.setIndentStyle(indentStyle); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig indentSize(int indentSize) { + options.setIndentSize(indentSize); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig lineLength(int lineLength) { + options.setLineLength(lineLength); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig wrapStyle(String wrapStyle) { + options.setWrapStyle(wrapStyle); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig closingParenOnNewLine(boolean closingParenOnNewLine) { + options.setClosingParenOnNewLine(closingParenOnNewLine); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig trailingCommas(boolean trailingCommas) { + options.setTrailingCommas(trailingCommas); + replaceStep(createStep()); + return this; + } + + public PrinceOfSpaceConfig javaLanguageLevel(int javaLanguageLevel) { + options.setJavaLanguageLevel(javaLanguageLevel); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return PrinceOfSpaceStep.create(version, provisioner(), options); + } + } + public EclipseConfig eclipse() { return eclipse(EclipseJdtFormatterStep.defaultVersion()); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java index 0432614091..9ddfb89eb0 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ public class JsonExtension extends FormatExtension { private static final int DEFAULT_INDENTATION = 4; - private static final String DEFAULT_ZJSONPATCH_VERSION = "0.4.14"; static final String NAME = "json"; @Inject @@ -216,7 +215,7 @@ public class JsonPatchConfig { private List> patch; public JsonPatchConfig(List> patch) { - this(DEFAULT_ZJSONPATCH_VERSION, patch); + this(JsonPatchStep.defaultVersion(), patch); } public JsonPatchConfig(String zjsonPatchVersion, List> patch) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java index 3c66f5427f..96dc160bbe 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java @@ -24,6 +24,7 @@ import javax.inject.Inject; import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; @@ -66,7 +67,8 @@ void setup() { taskService = SpotlessTaskService.registerIfAbsent(getProject(), compositeBuildSuffix); usesService(taskService); getBuildEventsListenerRegistry().onTaskCompletion(taskService); - unitOutput = new File(getProject().getLayout().getBuildDirectory().getAsFile().get(), "tmp/spotless-register-dependencies"); + // lazy, so a build directory set later in the buildscript still counts + getUnitOutput().set(getProject().getLayout().getBuildDirectory().file("tmp/spotless-register-dependencies")); } List steps = new ArrayList<>(); @@ -76,15 +78,12 @@ public List getSteps() { return steps; } - File unitOutput; - @OutputFile - public File getUnitOutput() { - return unitOutput; - } + public abstract RegularFileProperty getUnitOutput(); @TaskAction public void trivialFunction() throws IOException { + File unitOutput = getUnitOutput().get().getAsFile(); Files.createParentDirs(unitOutput); Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java index 824690e94e..a8b715cbf9 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,12 @@ import java.util.List; import org.gradle.api.GradleException; +import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.FileVisitDetails; import org.gradle.api.file.FileVisitor; +import org.gradle.api.initialization.IncludedBuild; +import org.gradle.api.invocation.Gradle; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; @@ -139,10 +142,67 @@ void init(TaskProvider impl) { getProjectPath().set(getProject().getPath()); getEncoding().set(impl.map(SpotlessTask::getEncoding)); getRunToFixMessage().convention( - "Run '" + calculateGradleCommand() + " spotlessApply' to fix all violations."); + "Run '" + calculateGradleCommand() + " " + spotlessApplySelector(getProject()) + "' to fix all violations."); } private static String calculateGradleCommand() { return FileSignature.machineIsWin() ? "gradlew.bat" : "./gradlew"; } + + /** + * Task selector for the fix-it hint. + *

    + *
  • In the main/root build, bare {@code spotlessApply} fixes every project (see #2592).
  • + *
  • In an included/composite build, bare task names do not select included-build tasks, + * so we must use the build-tree path (e.g. {@code :my-utils:spotlessApply}) โ€” see #2421.
  • + *
+ */ + static String spotlessApplySelector(Project project) { + if (project.getGradle().getParent() == null) { + return "spotlessApply"; + } + return buildTreePath(project) + ":spotlessApply"; + } + + /** + * Path of this project in the full build tree (includes included-build names). + * Uses {@link Project#getBuildTreePath()} when available (Gradle 8.10+), otherwise + * reconstructs it from {@link Gradle#getIncludedBuilds()}. + */ + static String buildTreePath(Project project) { + try { + Object treePath = Project.class.getMethod("getBuildTreePath").invoke(project); + if (treePath instanceof String s && !s.isEmpty()) { + return s; + } + } catch (ReflectiveOperationException ignored) { + // Gradle < 8.10 + } + return reconstructBuildTreePath(project); + } + + private static String reconstructBuildTreePath(Project project) { + StringBuilder buildPrefix = new StringBuilder(); + Gradle gradle = project.getGradle(); + for (Gradle parent = gradle.getParent(); parent != null; parent = gradle.getParent()) { + File rootDir = gradle.getRootProject().getRootDir().getAbsoluteFile(); + String buildName = null; + for (IncludedBuild included : parent.getIncludedBuilds()) { + if (included.getProjectDir().getAbsoluteFile().equals(rootDir)) { + buildName = included.getName(); + break; + } + } + if (buildName == null) { + buildName = gradle.getRootProject().getName(); + } + buildPrefix.insert(0, ":" + buildName); + gradle = parent; + } + String projectPath = project.getPath(); + if (":".equals(projectPath)) { + return buildPrefix.length() == 0 ? ":" : buildPrefix.toString(); + } + return buildPrefix + projectPath; + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java index 6cd3d15458..9166904fd4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,22 @@ */ package com.diffplug.gradle.spotless; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.inject.Inject; import org.gradle.api.DefaultTask; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecOperations; import org.gradle.work.DisableCachingByDefault; +import org.jspecify.annotations.NonNull; import com.diffplug.spotless.GitPrePushHookInstaller.GitPreHookLogger; import com.diffplug.spotless.GitPrePushHookInstallerGradle; @@ -45,14 +54,18 @@ public abstract class SpotlessInstallPrePushHookTask extends DefaultTask { @Internal abstract Property getIsRootExecution(); + @Inject + protected abstract ExecOperations getExecOperations(); + /** - * Executes the task to install the Git pre-push hook. + * Installs the Spotless pre-push hook for a supported Git repository. * - *

This method creates an instance of {@link GitPrePushHookInstallerGradle}, - * providing a logger to record informational and error messages during the installation process. - * The installer then installs the hook in the root directory of the Gradle project. + *

The task runs only for the root project. It asks Git to resolve the + * effective hook path, validates the destination, and delegates the actual + * installation to {@link GitPrePushHookInstallerGradle}. Unsupported + * repositories or destinations are logged and skipped. * - * @throws Exception if an error occurs during the hook installation process. + * @throws Exception if resolving the hook path or installing the hook fails */ @TaskAction public void performAction() throws Exception { @@ -62,6 +75,98 @@ public void performAction() throws Exception { return; } + File rootDir = getRootDir().get(); + + final boolean isWorkTree; + try { + isWorkTree = isGitWorkTree(rootDir); + } catch (org.gradle.api.GradleException exception) { + getLogger().warn("Skipping Spotless pre-push hook installation - could not run Git to check repository config {}: {}", + rootDir, exception.getMessage()); + return; + } + + if (!isWorkTree || !isSupportedGitRepository(rootDir)) { + getLogger().warn("Skipping Spotless pre-push hook installation - directory {} is not a supported Git repository", rootDir); + return; + } + + File prePushHookFile = resolvePrePushHookFile(rootDir); + File prePushHookRootDir = prePushHookFile.getParentFile(); + + if (!isSupportedDestinationDir(rootDir, prePushHookRootDir, prePushHookFile)) { + getLogger().warn( + "Skipping Spotless pre-push hook installation - not supported destination dir {}.", prePushHookRootDir); + return; + } + + getLogger().debug("Found pre push hook root from config: {}", prePushHookRootDir); + + createInstaller(rootDir, prePushHookRootDir).install(); + } + + private boolean isSupportedGitRepository(File root) { + return Files.isRegularFile(root.toPath().resolve(".git/config")); + } + + /** + * Checks whether the resolved hook destination is safe for this installer. + * + *

The hooks directory must be inside the project and must either exist + * as a directory or not exist yet. The pre-push hook must either not exist + * or be a regular file; symbolic links are rejected to avoid writing + * through them. + * + * @throws IOException if a canonical path cannot be resolved + */ + private boolean isSupportedDestinationDir(File rootDir, File hooksDir, File hookFile) throws IOException { + return hooksDir.getCanonicalFile().toPath().startsWith(rootDir.getCanonicalFile().toPath()) + && (!hooksDir.exists() || hooksDir.isDirectory()) + && !Files.isSymbolicLink(hookFile.toPath()) + && (!hookFile.exists() || hookFile.isFile()); + } + + private boolean isGitWorkTree(File root) { + var outputStream = new ByteArrayOutputStream(); + var errorStream = new ByteArrayOutputStream(); + + var result = getExecOperations().exec(spec -> { + spec.setWorkingDir(root); + spec.commandLine("git", "rev-parse", "--is-inside-work-tree"); + spec.setStandardOutput(outputStream); + spec.setErrorOutput(errorStream); + spec.setIgnoreExitValue(true); + }); + + return result.getExitValue() == 0 + && outputStream.toString(StandardCharsets.UTF_8).trim().equals("true"); + } + + private File resolvePrePushHookFile(File root) { + var outputStream = new ByteArrayOutputStream(); + + getExecOperations().exec(spec -> { + spec.setWorkingDir(root); + spec.commandLine("git", "rev-parse", "--git-path", "hooks/pre-push"); + spec.setStandardOutput(outputStream); + }); + + String pathOutput = outputStream.toString(StandardCharsets.UTF_8); + var path = Path.of(normalizePath(pathOutput)); + return (path.isAbsolute() ? path : root.toPath().resolve(path)).normalize().toFile(); + } + + private static @NonNull String normalizePath(String pathOutput) { + if (pathOutput.endsWith("\n")) { + pathOutput = pathOutput.substring(0, pathOutput.length() - 1); + if (pathOutput.endsWith("\r")) { + pathOutput = pathOutput.substring(0, pathOutput.length() - 1); + } + } + return pathOutput; + } + + private @NonNull GitPrePushHookInstallerGradle createInstaller(File rootDir, File prePushHookRootDir) { final var logger = new GitPreHookLogger() { @Override public void info(String format, Object... arguments) { @@ -79,7 +184,6 @@ public void error(String format, Object... arguments) { } }; - final var installer = new GitPrePushHookInstallerGradle(logger, getRootDir().get()); - installer.install(); + return new GitPrePushHookInstallerGradle(logger, rootDir, prePushHookRootDir); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java index 90f282d873..c6fd993d20 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -59,10 +59,11 @@ public abstract class SpotlessTaskService implements BuildService apply = Collections.synchronizedMap(new HashMap<>()); private final Map source = Collections.synchronizedMap(new HashMap<>()); private final Map provisioner = Collections.synchronizedMap(new HashMap<>()); - private final Map p2Provisioner = Collections.synchronizedMap(new HashMap<>()); @Nullable GradleProvisioner.DedupingProvisioner predeclaredProvisioner; @Nullable GradleProvisioner.DedupingP2Provisioner predeclaredP2Provisioner; + /** Shared across subprojects so parallel fingerprinting reuses one P2 cache + lock. */ + @Nullable private volatile GradleProvisioner.DedupingP2Provisioner sharedP2Provisioner; @Nullable RegisterDependenciesTask registerDependenciesTask; Provisioner provisionerFor(SpotlessExtension spotless) { @@ -84,12 +85,31 @@ P2Provisioner p2ProvisionerFor(SpotlessExtension spotless) { if (predeclaredP2Provisioner != null) { return predeclaredP2Provisioner.cachedOnly; } else { - return p2Provisioner.computeIfAbsent(spotless.project.getPath(), - unused -> new GradleProvisioner.DedupingP2Provisioner(P2Provisioner.createDefault(), GradleProvisioner.defaultP2CacheDirectory(spotless.project))); + // One DedupingP2Provisioner for the whole build (not per-project). Parallel + // multi-project fingerprinting of eclipse()/greclipse() steps otherwise races + // on Solstice's on-disk P2 cache โ€” Gradle then reports + // "ConfigurationCacheHackList cannot be serialized" (#3004). + return sharedP2Provisioner(spotless.project); } } } + private GradleProvisioner.DedupingP2Provisioner sharedP2Provisioner(Project project) { + GradleProvisioner.DedupingP2Provisioner local = sharedP2Provisioner; + if (local == null) { + synchronized (this) { + local = sharedP2Provisioner; + if (local == null) { + local = new GradleProvisioner.DedupingP2Provisioner( + P2Provisioner.createDefault(), + GradleProvisioner.defaultP2CacheDirectory(project)); + sharedP2Provisioner = local; + } + } + } + return local; + } + void registerSourceAlreadyRan(SpotlessTask task) { source.put(task.getPath(), task); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java index 0d5838c24a..a05fc44eeb 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -170,7 +170,7 @@ protected FormatterStep createStep() { private void fixParserToTypescript() { if (this.prettierConfig == null) { this.prettierConfig = new TreeMap<>(Map.of("parser", "typescript")); - } else { + } else if (!Objects.equals(this.prettierConfig.get("parser"), "typescript")) { final Object replaced = this.prettierConfig.put("parser", "typescript"); if (replaced != null) { getProject().getLogger().warn("overriding parser option to 'typescript'. Was set to '{}'", diff --git a/plugin-gradle/src/main/resources/META-INF/gradle-plugins/com.diffplug.gradle.spotless.properties b/plugin-gradle/src/main/resources/META-INF/gradle-plugins/com.diffplug.gradle.spotless.properties new file mode 100644 index 0000000000..dc8436ddaa --- /dev/null +++ b/plugin-gradle/src/main/resources/META-INF/gradle-plugins/com.diffplug.gradle.spotless.properties @@ -0,0 +1 @@ +implementation-class=com.diffplug.gradle.spotless.SpotlessPluginRedirect diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java index 2025de9b78..546d98c2c5 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/AsciidocExtensionTest.java @@ -31,7 +31,7 @@ void missingTargetFails() throws IOException { repositories { mavenCentral() } spotless { asciidoc { - adocfmt('0.2.0') + adocfmt('0.3.1') } } """); @@ -52,7 +52,7 @@ void integration() throws IOException { spotless { asciidoc { target 'test.adoc' - adocfmt('0.2.0') + adocfmt('0.3.1') .normalizeSetextHeadings(true) .collapseConsecutiveBlankLines(true) .oneSentencePerLine(true) @@ -64,6 +64,10 @@ void integration() throws IOException { .normalizeOrderedListMarkers(true) .ensureHeadingBlankLines(true) .ensureSourceDelimiters(true) + .formatTables(true) + .tableLayout('AUTO') + .tableMaxLineWidth(120) + .tableBlankLines('ALL') } } """); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CompositeBuildRunToFixMessageTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CompositeBuildRunToFixMessageTest.java new file mode 100644 index 0000000000..1d77bc36c9 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CompositeBuildRunToFixMessageTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FileSignature; + +/** + * Regression for #2421: + * when Spotless runs inside an included build of a composite, the run-to-fix hint must + * use the build-tree task path (e.g. {@code :my-utils:spotlessApply}), not a bare + * {@code spotlessApply} which only selects tasks in the root/main build. + */ +class CompositeBuildRunToFixMessageTest extends GradleIntegrationHarness { + + private static String expectedGradleCommand() { + return FileSignature.machineIsWin() ? "gradlew.bat" : "./gradlew"; + } + + @Test + void includedBuildRootSuggestsBuildTreePath() throws IOException { + createCompositeWithIncluded("my-utils", null); + + String output = gradleRunner() + .withArguments(":my-utils:spotlessCheck") + .buildAndFail() + .getOutput(); + + Assertions.assertThat(output) + .contains("Run '" + expectedGradleCommand() + " :my-utils:spotlessApply' to fix all violations."); + } + + @Test + void includedBuildSubprojectSuggestsBuildTreePath() throws IOException { + createCompositeWithIncluded("my-utils", "lib"); + + String output = gradleRunner() + .withArguments(":my-utils:lib:spotlessCheck") + .buildAndFail() + .getOutput(); + + Assertions.assertThat(output) + .contains("Run '" + expectedGradleCommand() + " :my-utils:lib:spotlessApply' to fix all violations."); + } + + @Test + void standaloneBuildStillSuggestsBareSpotlessApply() throws IOException { + // Outside a composite, bare spotlessApply is correct and preferred (#2592). + setFile("settings.gradle").toContent("rootProject.name = 'standalone'"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " format 'misc', {", + " target 'test.txt'", + " leadingTabsToSpaces(2)", + " }", + "}"); + setFile("test.txt").toContent("\thello\n"); + + String output = gradleRunner() + .withArguments("spotlessCheck") + .buildAndFail() + .getOutput(); + + Assertions.assertThat(output) + .contains("Run '" + expectedGradleCommand() + " spotlessApply' to fix all violations.") + .doesNotContain(":spotlessApply"); + } + + private void createCompositeWithIncluded(String includedName, String subproject) throws IOException { + setFile("settings.gradle").toLines( + "rootProject.name = 'my-composite'", + "includeBuild('" + includedName + "')"); + setFile("build.gradle").toContent(""); + + setFile(includedName + "/settings.gradle").toContent( + subproject == null + ? "rootProject.name = '" + includedName + "'" + : "rootProject.name = '" + includedName + "'\ninclude '" + subproject + "'"); + + String spotlessBlock = String.join("\n", + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " format 'misc', {", + " target 'test.txt'", + " leadingTabsToSpaces(2)", + " }", + "}"); + + if (subproject == null) { + setFile(includedName + "/build.gradle").toContent(spotlessBlock); + setFile(includedName + "/test.txt").toContent("\thello\n"); + } else { + setFile(includedName + "/build.gradle").toContent(""); + setFile(includedName + "/" + subproject + "/build.gradle").toContent(spotlessBlock); + setFile(includedName + "/" + subproject + "/test.txt").toContent("\thello\n"); + } + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GitRatchetGradleTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GitRatchetGradleTest.java index 0e3cb3f283..be48e23b9d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GitRatchetGradleTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GitRatchetGradleTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ void singleProjectExhaustive(int useConfigCache) throws Exception { "}"); setFile(TEST_PATH).toContent("HELLO"); git.add().addFilepattern(TEST_PATH).call(); - git.commit().setMessage("Initial state").call(); + git.commit().setSign(false).setMessage("Initial state").call(); // tag this initial state as the baseline for spotless to ratchet from git.tag().setName("baseline").call(); @@ -271,7 +271,7 @@ public BuildResultAssertion outcome(String taskPath, TaskOutcome expected) { private RevCommit addAndCommit(Git git) throws NoFilepatternException, GitAPIException { PersonIdent emptyPerson = new PersonIdent("jane doe", "jane@doe.com", new Date(0), TimeZone.getTimeZone("UTC")); git.add().addFilepattern(".").call(); - return git.commit().setMessage("baseline") + return git.commit().setSign(false).setMessage("baseline") .setCommitter(emptyPerson) .setAuthor(emptyPerson) .call(); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoogleJavaFormatIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoogleJavaFormatIntegrationTest.java index e7fc9d4b74..dde872d3e5 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoogleJavaFormatIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoogleJavaFormatIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,11 @@ import org.junit.jupiter.api.Test; +import com.diffplug.spotless.Jvm; + class GoogleJavaFormatIntegrationTest extends GradleIntegrationHarness { + private static final String NON_DEFAULT_VERSION = Jvm.version() <= 17 ? "1.27.0" : "1.34.1"; + @Test void integration() throws IOException { setFile("build.gradle").toLines( @@ -31,7 +35,7 @@ void integration() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat('" + NON_DEFAULT_VERSION + "')", " }", "}"); @@ -41,7 +45,7 @@ void integration() throws IOException { checkRunsThenUpToDate(); replace("build.gradle", - "googleJavaFormat('1.17.0')", + "googleJavaFormat('" + NON_DEFAULT_VERSION + "')", "googleJavaFormat()"); checkRunsThenUpToDate(); } @@ -57,7 +61,7 @@ void integrationWithReorderImports() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0').aosp().reorderImports(true)", + " googleJavaFormat('" + NON_DEFAULT_VERSION + "').aosp().reorderImports(true)", " }", "}"); @@ -67,7 +71,7 @@ void integrationWithReorderImports() throws IOException { checkRunsThenUpToDate(); replace("build.gradle", - "googleJavaFormat('1.17.0')", + "googleJavaFormat('" + NON_DEFAULT_VERSION + "')", "googleJavaFormat()"); checkRunsThenUpToDate(); } @@ -83,7 +87,7 @@ void integrationWithSkipJavadocFormatting() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0').skipJavadocFormatting()", + " googleJavaFormat('" + NON_DEFAULT_VERSION + "').skipJavadocFormatting()", " }", "}"); @@ -93,7 +97,7 @@ void integrationWithSkipJavadocFormatting() throws IOException { checkRunsThenUpToDate(); replace("build.gradle", - "googleJavaFormat('1.17.0')", + "googleJavaFormat('" + NON_DEFAULT_VERSION + "')", "googleJavaFormat()"); checkRunsThenUpToDate(); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java index 0e223a3409..c29d98329d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java @@ -55,9 +55,12 @@ public enum GradleVersionSupport { GradleVersionSupport(String version) { String minVersionForRunningJRE; switch (Jvm.version()) { - case 26: - throw new IllegalStateException("Gradle does not yet support running on Java 26. " + + case 27: + throw new IllegalStateException("Gradle does not yet support running on Java 27. " + "You need to update the case ref to https://docs.gradle.org/current/userguide/compatibility.html."); + case 26: + minVersionForRunningJRE = "9.4.0"; + break; case 25: minVersionForRunningJRE = "9.1.0"; break; diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/HackCloneRaceIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/HackCloneRaceIntegrationTest.java new file mode 100644 index 0000000000..4e18042bdc --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/HackCloneRaceIntegrationTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +/** + * End-to-end regression for {@code HackClone.writeObject} with Gradle task input + * fingerprinting. + * + *

The original failure used: + * + *


+ * groovyGradle {
+ *     toggleOffOn()                // adds a FenceStep
+ *     target '**/*.gradle'     // no files matching this target
+ *     greclipse()                  // lazy slow step
+ * }
+ * 
+ * + *

{@code toggleOffOn()} creates a wrapper step that preserves text between the off/on markers and + * stores {@code greclipse()} in a lazy {@code FormatterStep} containing a nested + * {@code ConfigurationCacheHackList}. As no files match the target, the slow {@code greclipse} state + * is first initialized while Gradle fingerprints task inputs, before task actions format any files. + * + *

The race is not specific to {@code greclipse} formatting. It only needs a lazy {@code FormatterStep} + * whose state supplier stays inside {@code HackClone.writeObject} long enough for another Gradle + * worker to serialize the same half-built clone. {@code greclipse()} provided that delay by resolving + * Eclipse state. + * + *

The real build configures a separate {@code greclipse()} step for each subproject. This test + * deliberately reuses one local slow no-op wrapper step across subprojects so parallel Gradle workers + * serialize the same nested {@code HackClone}. That makes the concurrency window deterministic while + * avoiding Eclipse provisioning and still using Gradle's real input serialization path. + * + *

That shared-object setup is intentionally not an isolated-projects-compatible build pattern: + * isolated projects remove this kind of cross-project shared configuration state, while this + * reproducer needs it to force concurrent serialization of one {@code HackClone}. + */ +class HackCloneRaceIntegrationTest extends GradleIntegrationHarness { + private static final int SUBPROJECTS = 12; + private static final int ATTEMPTS = 3; + private static final int STATE_INIT_SLEEP_MS = 500; + + @Test + void realGradleParallelFingerprintingOfNestedLazyStepIsThreadSafe() throws IOException { + StringBuilder settings = new StringBuilder("rootProject.name = 'race'\n"); + for (int i = 0; i < SUBPROJECTS; i++) { + settings.append("include 'sub").append(i).append("'\n"); + setFile("sub" + i + "/.keep").toContent(""); + } + setFile("settings.gradle").toContent(settings.toString()); + setFile("gradle.properties").toContent("org.gradle.jvmargs=-Xmx1g\n"); + + setFile("build.gradle").toContent(String.join("\n", + "plugins { id 'com.diffplug.spotless' }", + "", + "class SlowStateSupplier implements com.diffplug.spotless.ThrowingEx.Supplier, Serializable {", + " private final long sleepMillis", + " SlowStateSupplier(long sleepMillis) { this.sleepMillis = sleepMillis }", + " String get() { Thread.sleep(sleepMillis); return 'state' }", + "}", + "", + "class NoopFormatter implements com.diffplug.spotless.FormatterFunc, Serializable {", + " String apply(String input) { return input }", + "}", + "", + "def hackCloneRaceStep", + "", + "subprojects {", + " apply plugin: 'com.diffplug.spotless'", + " spotless {", + " if (hackCloneRaceStep == null) {", + " def slowStep = com.diffplug.spotless.FormatterStep.createLazy(", + " 'slowLazy',", + " new SlowStateSupplier(" + STATE_INIT_SLEEP_MS + "L),", + " com.diffplug.spotless.SerializedFunction.identity(),", + " com.diffplug.spotless.SerializedFunction.alwaysReturns(new NoopFormatter()))", + " hackCloneRaceStep = com.diffplug.spotless.generic.FenceStep", + " .named('toggle')", + " .openClose('spotless:off', 'spotless:on')", + " .preserveWithin([slowStep])", + " }", + " format 'race', {", + " target 'no-such-dir/**/*.txt'", + " addStep(hackCloneRaceStep)", + " }", + " }", + "}")); + + for (int attempt = 0; attempt < ATTEMPTS; attempt++) { + BuildResult result = gradleRunner() + .withGradleVersion("9.6.1") + .withArguments("spotlessRace", "--parallel", "--build-cache", "--rerun-tasks", "--max-workers=" + SUBPROJECTS, "--stacktrace") + .build(); + assertThat(result.getOutput()) + .as("attempt %d must not trip the HackClone.writeObject guard", attempt) + .doesNotContain("roundtripStateInternal or equalityStateInternal should be non-null"); + } + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java index 71fd2771e0..60762dce8f 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaDefaultTargetTest.java @@ -176,6 +176,38 @@ void expandWildCardImports() throws IOException { assertFile("src/main/java/foo/bar/JavaCodeWildcardsUnformatted.java").sameAsResource("java/expandwildcardimports/JavaClassWithWildcardsFormatted.test"); } + @Test + void expandWildcardImportsIgnoresUnrelatedConfigurations() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "", + "repositories { mavenCentral() }", + "", + "configurations {", + " leftover {", + " canBeResolved = true", + " canBeConsumed = false", + " }", + "}", + "", + "dependencies {", + " leftover 'does.not:exist:1.0'", + "}", + "", + "spotless {", + " java {", + " target file('src/main/java/test.java')", + " expandWildcardImports()", + " }", + "}"); + setFile("src/main/java/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/java/test.java").sameAsResource("java/googlejavaformat/JavaCodeUnformatted.test"); + } + /** * Triggers the special case in {@link FormatExtension#setupTask(SpotlessTask)} with {@code toggleFence} and * {@code targetExcludeContentPattern} both being not {@code null}. diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java index f986d83717..610a35c9f7 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ class KotlinExtensionTest extends GradleIntegrationHarness { void integrationDiktat() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -49,7 +49,7 @@ void integrationKtfmtDropboxStyleWithPublicApi() throws IOException { setFile("build.gradle.kts").toLines( "import com.diffplug.spotless.kotlin.KtfmtStep.TrailingCommaManagementStrategy", "plugins {", - " id(\"org.jetbrains.kotlin.jvm\") version \"1.6.21\"", + " id(\"org.jetbrains.kotlin.jvm\") version \"2.1.10\"", " id(\"com.diffplug.spotless\")", "}", "repositories { mavenCentral() }", @@ -73,7 +73,7 @@ void integrationKtfmtDropboxStyleWithPublicApi() throws IOException { void withExperimentalEditorConfigOverride() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -97,7 +97,7 @@ void testWithInvalidEditorConfigFile() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -117,7 +117,7 @@ void testReadCodeStyleFromEditorConfigFile() throws IOException { setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -134,7 +134,7 @@ void testEditorConfigOverrideWithUnsetCodeStyleDoesNotOverrideEditorConfigCodeSt setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -153,7 +153,7 @@ void testSetEditorConfigCanOverrideEditorConfigFile() throws IOException { setFile(".editorconfig").toResource("kotlin/ktlint/intellij_idea/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -171,7 +171,7 @@ void testSetEditorConfigCanOverrideEditorConfigFile() throws IOException { void withCustomRuleSetApply() throws IOException { setFile("build.gradle.kts").toLines( "plugins {", - " id(\"org.jetbrains.kotlin.jvm\") version \"1.6.21\"", + " id(\"org.jetbrains.kotlin.jvm\") version \"2.1.10\"", " id(\"com.diffplug.spotless\")", "}", "repositories { mavenCentral() }", @@ -195,7 +195,7 @@ void withCustomRuleSetApply() throws IOException { void testWithHeader() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -214,7 +214,7 @@ void testWithHeader() throws IOException { void testWithCustomMaxWidthDefaultStyleKtfmt() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java index c607c36912..b356bef180 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ class KotlinGradleExtensionTest extends KotlinExtensionTest { void testTarget(boolean useDefaultTarget) throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", + " id 'org.jetbrains.kotlin.jvm' version '2.1.10'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java index 5071762b5f..fc8d63e4a9 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/LicenseHeaderTest.java @@ -163,7 +163,7 @@ void filterByContentPatternTest() throws IOException { @Test void ratchetFrom() throws Exception { try (Git git = Git.init().setDirectory(rootFolder()).call()) { - git.commit().setMessage("First commit").call(); + git.commit().setSign(false).setMessage("First commit").call(); } setLicenseStep("licenseHeader('/** $YEAR */')\nratchetFrom 'HEAD'"); testSuiteUpdateWithLatest(true); @@ -172,7 +172,7 @@ void ratchetFrom() throws Exception { @Test void ratchetFromButUpdateFalse() throws Exception { try (Git git = Git.init().setDirectory(rootFolder()).call()) { - git.commit().setMessage("First commit").call(); + git.commit().setSign(false).setMessage("First commit").call(); } try (Git ignored = Git.init().setDirectory(rootFolder()).call()) { setLicenseStep("licenseHeader('/** $YEAR */').updateYearWithLatest(false)\nratchetFrom 'HEAD'"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java index 3403bbfaad..5a9045ac06 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import com.diffplug.common.base.StringPrinter; +import com.diffplug.spotless.java.GoogleJavaFormatStep; class MultiProjectTest extends GradleIntegrationHarness { private static final int N = 100; @@ -47,7 +48,7 @@ void createSubproject(String name) throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat()", " }", "}"); setFile(name + "/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -71,7 +72,7 @@ public void hasRootSpotless() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat()", " }", "}"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -88,7 +89,7 @@ public void predeclaredFails() throws IOException { "spotless { predeclareDeps() }"); createNSubprojects(); Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) - .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.17.0] into the `spotlessPredeclare` block in the root project."); + .contains("Add a step with [com.google.googlejavaformat:google-java-format:" + GoogleJavaFormatStep.defaultVersion() + "] into the `spotlessPredeclare` block in the root project."); } @Test @@ -101,7 +102,7 @@ public void predeclaredSucceeds_deprecatedAPI() throws IOException { "spotless { predeclareDeps() }", "", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); @@ -119,7 +120,7 @@ public void predeclaredFromBuildscriptSucceeds_deprecatedAPI() throws IOExceptio "repositories { mavenCentral() }", "spotless { predeclareDepsFromBuildscript() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); @@ -138,7 +139,7 @@ public void predeclaredFromBuildscriptInPredeclareBlockSucceeds() throws IOExcep "", "spotlessPredeclare {", " fromBuildscriptRepositories()", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); @@ -152,7 +153,7 @@ public void predeclaredOrderingIsFlexible() throws IOException { "}", "repositories { mavenCentral() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}", "spotless { predeclareDeps() }"); createNSubprojects(); @@ -169,11 +170,11 @@ public void predeclaredDepsRegression() throws IOException { " predeclareDeps()", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat()", " }", "}", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); setFile("sub/build.gradle").toLines( @@ -182,11 +183,11 @@ public void predeclaredDepsRegression() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat()", " }", "}"); setFile("sub/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); - gradleRunner().withGradleVersion("8.14").withArguments("spotlessApply").build(); + gradleRunner().withArguments("spotlessApply").build(); } @Test @@ -197,7 +198,7 @@ public void predeclaredWithoutSpotlessBlockSucceeds() throws IOException { "}", "repositories { mavenCentral() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat() }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java index b41331765c..2df13ef405 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,13 @@ void useNodeAndNpmFromNodeGradlePlugin() throws Exception { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", - " id 'com.github.node-gradle.node' version '3.5.1'", + " id 'com.github.node-gradle.node' version '7.1.0'", "}", "repositories { mavenCentral() }", "node {", " download = true", - " version = '18.16.1'", - " npmVersion = '9.5.1'", + " version = '22.14.0'", + " npmVersion = '10.9.2'", " workDir = file(\"${buildDir}/nodejs\")", " npmWorkDir = file(\"${buildDir}/npm\")", "}", @@ -106,12 +106,12 @@ void useNpmFromNodeGradlePlugin() throws Exception { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", - " id 'com.github.node-gradle.node' version '3.5.1'", + " id 'com.github.node-gradle.node' version '7.1.0'", "}", "repositories { mavenCentral() }", "node {", " download = true", - " version = '18.16.1'", + " version = '22.14.0'", " workDir = file(\"${buildDir}/nodejs\")", "}", "def prettierConfig = [:]", @@ -145,12 +145,12 @@ void useNpmNextToConfiguredNodePluginFromNodeGradlePlugin() throws Exception { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", - " id 'com.github.node-gradle.node' version '3.5.1'", + " id 'com.github.node-gradle.node' version '7.1.0'", "}", "repositories { mavenCentral() }", "node {", " download = true", - " version = '18.13.0'", + " version = '22.14.0'", " workDir = file(\"${buildDir}/nodejs\")", "}", "def prettierConfig = [:]", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PalantirJavaFormatIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PalantirJavaFormatIntegrationTest.java index 9990039cd3..02538ff763 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PalantirJavaFormatIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PalantirJavaFormatIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 DiffPlug + * Copyright 2022-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ void formatJavaDoc() throws IOException { "spotless {", " java {", " target file('test.java')", - " palantirJavaFormat('2.39.0').formatJavadoc(true)", + " palantirJavaFormat().formatJavadoc(true)", " }", "}"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskBuildDirTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskBuildDirTest.java new file mode 100644 index 0000000000..aec9a3a830 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskBuildDirTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class RegisterDependenciesTaskBuildDirTest extends GradleIntegrationHarness { + @Test + void unitOutputFollowsCustomBuildDirectory() throws IOException { + setFile("settings.gradle").toLines( + "rootProject.name = 'buildDirTest'", + "include 'sub'"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless { predeclareDeps() }", + "", + "spotlessPredeclare {", + " java { googleJavaFormat() }", + "}", + "", + "layout.buildDirectory = layout.projectDirectory.dir('custom-build')"); + setFile("sub/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " java {", + " target 'src/main/java/**/*.java'", + " googleJavaFormat()", + " }", + "}"); + setFile("sub/src/main/java/Hello.java").toLines( + "public class Hello {}"); + + gradleRunner().withArguments("spotlessApply").build(); + + Assertions.assertThat(newFile("custom-build/tmp/spotless-register-dependencies")) + .as("register-dependencies output should follow the configured build directory") + .exists(); + Assertions.assertThat(newFile("build/tmp/spotless-register-dependencies")) + .as("nothing should be written under the default build directory") + .doesNotExist(); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java index f739ab0c63..2ad638e2d1 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.file.Files; + import org.junit.jupiter.api.Test; class SpotlessInstallPrePushHookTaskTest extends GradleIntegrationHarness { @@ -24,20 +27,12 @@ class SpotlessInstallPrePushHookTaskTest extends GradleIntegrationHarness { @Test public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { // given - setFile(".git/config").toContent(""); + gitCommand("init", "--quiet"); newFile(".git/hooks").mkdirs(); - setFile("build.gradle").toLines( - "plugins {", - " id 'java'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }"); + setGradleFile(); // when - var output = gradleRunner() - .withArguments("spotlessInstallGitPrePushHook", "--system-prop=org.gradle.configuration-cache=true") - .build() - .getOutput(); + var output = getExecutionOutput(); // then assertThat(output).contains("Installing git pre-push hook"); @@ -54,20 +49,12 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws @Test public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { // given - setFile(".git/config").toContent(""); - setFile("build.gradle").toLines( - "plugins {", - " id 'java'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }"); + gitCommand("init", "--quiet"); setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); + setGradleFile(); // when - final var output = gradleRunner() - .withArguments("spotlessInstallGitPrePushHook", "--system-prop=org.gradle.configuration-cache=true") - .build() - .getOutput(); + final var output = getExecutionOutput(); // then assertThat(output).contains("Installing git pre-push hook"); @@ -79,4 +66,122 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro .replace("${applyCommand}", "spotlessApply"); assertFile(".git/hooks/pre-push").hasContent(content); } + + @Test + public void should_use_hooksPath_if_set_in_config() throws Exception { + // given + gitCommand("init", "--quiet"); + gitCommand("config", "--local", "core.hooksPath", ".githooks"); + setGradleFile(); + + // when + final var output = getExecutionOutput(); + + // then + assertThat(output).contains("Installing git pre-push hook"); + assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".githooks/pre-push")); + + final var content = getTestResource("git_pre_hook/pre-push.created-tpl") + .replace("${executor}", "gradle") + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); + assertFile(".githooks/pre-push").hasContent(content); + } + + @Test + public void should_skip_when_not_in_git_worktree() throws Exception { + setGradleFile(); + + // when + final var output = getExecutionOutput(); + + // then + assertThat(output).containsPattern("Skipping Spotless pre-push hook installation - directory .* is not a supported Git repository"); + } + + @Test + public void should_skip_when_prehook_config_not_in_parent_project() throws Exception { + gitCommand("init", "--quiet"); + var outsideHooks = rootFolder().toPath().resolveSibling(rootFolder().getName() + "-outside-hooks"); + gitCommand("config", "--local", "core.hooksPath", outsideHooks.toString()); + setGradleFile(); + + // when + final var output = getExecutionOutput(); + + // then + assertThat(outsideHooks).doesNotExist(); + assertThat(output).containsPattern("Skipping Spotless pre-push hook installation - not supported destination dir"); + } + + @Test + public void should_skip_when_prehook_file_a_directory() throws Exception { + gitCommand("init", "--quiet"); + gitCommand("config", "--local", "core.hooksPath", ".githooks"); + Files.createDirectories(newFile(".githooks/pre-push").toPath()); + setGradleFile(); + + // when + final var output = getExecutionOutput(); + + // then + assertThat(output).containsPattern("Skipping Spotless pre-push hook installation - not supported destination dir"); + } + + @Test + public void should_skip_when_prehook_config_a_symlink() throws Exception { + gitCommand("init", "--quiet"); + gitCommand("config", "--local", "core.hooksPath", ".githooks"); + + var target = setFile("target-hook").toContent("original content"); + Files.createDirectories(newFile(".githooks").toPath()); + + var hook = newFile(".githooks/pre-push").toPath(); + Files.createSymbolicLink(hook, target.toPath()); + + setGradleFile(); + + // when + final var output = getExecutionOutput(); + + // then + assertThat(Files.isSymbolicLink(hook)).isTrue(); + assertThat(Files.readString(target.toPath())).isEqualTo("original content"); + assertThat(output).containsPattern("Skipping Spotless pre-push hook installation - not supported destination dir"); + } + + private String getExecutionOutput() throws IOException { + return gradleRunner() + .withArguments("spotlessInstallGitPrePushHook", "--system-prop=org.gradle.configuration-cache=true") + .build() + .getOutput(); + } + + private void setGradleFile() { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }"); + } + + private void gitCommand(String... args) throws Exception { + var command = new java.util.ArrayList(); + command.add("git"); + command.addAll(java.util.List.of(args)); + + var process = new ProcessBuilder(command) + .directory(rootFolder()) + .redirectErrorStream(true) + .start(); + + String output; + try (var stream = process.getInputStream()) { + output = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + assertThat(process.waitFor()) + .as("git %s: %s", String.join(" ", args), output) + .isZero(); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java index 796c8896ec..c765d46fd1 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.java.GoogleJavaFormatStep; + /** * Integration tests for the spotlessPredeclare feature, which allows dependencies * to be predeclared in the root project and reused across all subprojects to avoid @@ -41,7 +43,7 @@ void predeclareSucceedsWithMavenDependencies() throws IOException { repositories { mavenCentral() } spotless { predeclareDeps() } spotlessPredeclare { - java { googleJavaFormat('1.17.0') } + java { googleJavaFormat() } } """); setFile("settings.gradle").toContent("include 'sub'"); @@ -53,7 +55,7 @@ void predeclareSucceedsWithMavenDependencies() throws IOException { spotless { java { target file('test.java') - googleJavaFormat('1.17.0') + googleJavaFormat() } } """); @@ -85,7 +87,7 @@ void predeclareFailsWhenDependencyNotPredeclared() throws IOException { spotless { java { target file('test.java') - googleJavaFormat('1.17.0') + googleJavaFormat() } } """); @@ -93,7 +95,7 @@ target file('test.java') BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); assertThat(result.getOutput()) - .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.17.0]") + .contains("Add a step with [com.google.googlejavaformat:google-java-format:" + GoogleJavaFormatStep.defaultVersion() + "]") .contains("into the `spotlessPredeclare` block in the root project"); } @@ -106,7 +108,7 @@ void predeclareWorksWithMultipleVersions() throws IOException { repositories { mavenCentral() } spotless { predeclareDeps() } spotlessPredeclare { - java { googleJavaFormat('1.17.0') } + java { googleJavaFormat() } } """); setFile("settings.gradle").toContent("include 'sub1', 'sub2'"); @@ -118,7 +120,7 @@ void predeclareWorksWithMultipleVersions() throws IOException { spotless { java { target file('test.java') - googleJavaFormat('1.17.0') + googleJavaFormat() } } """); @@ -131,7 +133,7 @@ target file('test.java') spotless { java { target file('test.java') - googleJavaFormat('1.17.0') + googleJavaFormat() } } """); @@ -209,9 +211,13 @@ target file('test.java') setFile("sub/test.java").toResource("java/eclipse/JavaCodeUnformatted.test"); BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + // Eclipse JDT resolves from an embedded lockfile rather than a P2 query, so an + // un-predeclared subproject fails on the Maven coordinates instead of on P2. + // greclipse still has no lockfile, so it keeps the P2 message - see + // GroovyDependencies.predeclareFailsWhenGroovyGrEclipseNotPredeclared. assertThat(result.getOutput()) - .contains("P2 dependencies not predeclared") - .contains("Add Eclipse formatter configuration to the `spotlessPredeclare` block in the root project"); + .contains("into the `spotlessPredeclare` block in the root project") + .contains("org.eclipse.jdt:org.eclipse.jdt.core"); } @Test @@ -465,7 +471,7 @@ void predeclareWorksWithBothMavenAndP2Dependencies() throws IOException { spotless { predeclareDeps() } spotlessPredeclare { java { - googleJavaFormat('1.17.0') + googleJavaFormat() eclipse() } } @@ -479,7 +485,7 @@ void predeclareWorksWithBothMavenAndP2Dependencies() throws IOException { spotless { java { target file('test.java') - googleJavaFormat('1.17.0') + googleJavaFormat() } } """); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java new file mode 100644 index 0000000000..8b2010d1bc --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +class TargetExcludeDirectoryTest extends GradleIntegrationHarness { + @Test + void targetExcludeWithDirectory() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude layout.projectDirectory.dir('generated')", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("generated/nested/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("generated/nested/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryProperty() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "def excluded = objects.directoryProperty()", + "excluded.set(layout.projectDirectory.dir('generated'))", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude excluded", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryProvider() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude providers.provider { layout.projectDirectory.dir('generated') }", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryAndConfigurationCache() throws IOException { + setFile("gradle.properties").toLines("org.gradle.configuration-cache=true"); + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude layout.projectDirectory.dir('generated')", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + + // the exclusion still applies on a second run with the cache enabled + setFile("manual.md").toLines("D E F"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("d e f"); + } + +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java index 3504a70e49..28ebf2938b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2025 DiffPlug + * Copyright 2020-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,7 +104,7 @@ void gjf() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.17.0')", + " googleJavaFormat()", " toggleOffOn()", " }", "}"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java index 0882d0f944..9f1988a624 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ import java.io.IOException; +import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -209,4 +211,49 @@ void useEslintStandardWithTypescriptRules() throws IOException { gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); assertFile("test.ts").sameAsResource("npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean"); } + + @Test + void prettierDoesNotWarnWhenTypescriptParserIsAlreadySet() throws IOException { + setFile(".prettierrc.json").toContent("{}"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " prettier().npmInstallCache().configFile('.prettierrc.json')", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/filetypes/typescript/typescript.dirty"); + + BuildResult result = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + + Assertions.assertThat(result.getOutput()) + .doesNotContain("overriding parser option to 'typescript'. Was set to 'typescript'"); + assertFile("test.ts").sameAsResource("npm/prettier/filetypes/typescript/typescript.clean"); + } + + @Test + void prettierWarnsWhenOverridingNonTypescriptParser() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " prettier().config(['parser': 'babel'])", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/filetypes/typescript/typescript.dirty"); + + BuildResult result = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + + Assertions.assertThat(result.getOutput()) + .contains("overriding parser option to 'typescript'. Was set to 'babel'"); + assertFile("test.ts").sameAsResource("npm/prettier/filetypes/typescript/typescript.clean"); + } } diff --git a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle index c0fa255bcc..67789283e3 100644 --- a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle @@ -4,13 +4,13 @@ */ plugins { id 'com.diffplug.spotless' - id 'com.github.node-gradle.node' version '3.5.1' + id 'com.github.node-gradle.node' version '7.1.0' } repositories { mavenCentral() } node { download = true - version = '18.16.1' - npmVersion = '9.5.1' + version = '22.14.0' + npmVersion = '10.9.2' // when setting both these directories, npm and node will be in separate directories workDir = file("${buildDir}/nodejs") npmWorkDir = file("${buildDir}/npm") diff --git a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle index 6d77bc3d69..59f694b299 100644 --- a/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle @@ -4,12 +4,12 @@ */ plugins { id 'com.diffplug.spotless' - id 'com.github.node-gradle.node' version '3.5.1' + id 'com.github.node-gradle.node' version '7.1.0' } repositories { mavenCentral() } node { download = true - version = '18.16.1' + version = '22.14.0' // when not setting an explicit `npmWorkDir`, the npm binary will be installed next to the node binary workDir = file("${buildDir}/nodejs") } diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index f2a4a75566..7ed1071e86 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -4,6 +4,81 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +## [3.10.4] - 2026-10-08 + +### Changes +- Bump default `eclipse` version from `4.40` to `4.41`. +- Bump default `gherkin` version `42.0.0` -> `42.0.1`. ([#3088](https://github.com/diffplug/spotless/pull/3088)) +- Bump default `jackson` version `2.22.2` -> `2.22.3`. ([#3101](https://github.com/diffplug/spotless/pull/3101)) +- Bump default `palantir-java-format` version `2.98.0` -> `2.102.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103), [#3132](https://github.com/diffplug/spotless/pull/3132)) +- Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) +- Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. +- Bump default `palantir-java-format` version `2.99.0` -> `2.102.0`. ([#3132](https://github.com/diffplug/spotless/pull/3132)) + +### Fixed +- Migrate Plexus component injection to JSR-330 and Maven parameter. ([#3122](https://github.com/diffplug/spotless/pull/3122)) +- `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) +- `expandWildcardImports` no longer drops the import of a type that is only used to qualify a static member (such as `Collections.sort(list)` or `TimeUnit.SECONDS`), or the static import of a field or enum constant (such as `PI` from `import static java.lang.Math.*`), which produced code that did not compile. ([#2833](https://github.com/diffplug/spotless/issues/2833)) + +## [3.10.3] - 2026-09-25 +### Changes +- Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `gson` version `2.13.2` -> `2.14.0`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `zjsonpatch` version `0.4.14` -> `0.4.16`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `jackson-dataformat-yaml` version `2.14.1` -> `2.20.1`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) +- Bump default `ktfmt` version `0.63` -> `0.64`. ([2988](https://github.com/diffplug/spotless/pull/2988)) +- Bump default `cleanthat` version `2.25` -> `2.26`. ([#2882](https://github.com/diffplug/spotless/pull/2882)) +- Bump default `jackson` version `2.20.1` -> `2.22.2`. ([#2819](https://github.com/diffplug/spotless/pull/2819)) +- Bump default `javaparser` version `3.27.1` -> `3.28.2`. ([#3065](https://github.com/diffplug/spotless/pull/3065)) +- Bump default `palantir-java-format` version `2.80.0` -> `2.98.0`. ([#3068](https://github.com/diffplug/spotless/pull/3068)) +- Bump default `scalafmt` version `3.8.1` -> `3.11.5`. ([#2173](https://github.com/diffplug/spotless/pull/2173)) +- Bump default `google-java-format` version `1.30.0` -> `1.36.1`. ([#3075](https://github.com/diffplug/spotless/pull/3075)) +- Bump default `gherkin-utils` version `10.0.0` -> `12.0.2`. ([#2979](https://github.com/diffplug/spotless/pull/2979)) + +### Fixed +- Fix release signing by using Gradle's required eight-digit signing subkey ID. ([#3105](https://github.com/diffplug/spotless/pull/3105)) +- Fix race when creating the npm install cache directory. (([#3096](https://github.com/diffplug/spotless/pull/3096)) +- GrEclipse no longer emits expected OSGi and nested-jar warnings during initialization. ([#2445](https://github.com/diffplug/spotless/issues/2445)) +- `typescript` `prettier()` no longer emits a warning when its parser is already set to `typescript`. ([#3098](https://github.com/diffplug/spotless/pull/3098)) +- `` preserves standalone comments at section boundaries and the end of the file. ([#3048](https://github.com/diffplug/spotless/issues/3048)) +- `` preserves entries when comments contain unmatched brackets, preserves commas inside quoted strings, and keeps significant line boundaries in multiline entries. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- `` now reports unfinished entries as lints at their starting line. These fail formatting by default, so upgrading may expose catalog errors that previously caused silent data loss. ([#3042](https://github.com/diffplug/spotless/pull/3042)) +- Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` or `NoSuchMethodError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [3.10.2] - 2026-09-04 +### Fixed +- `` now shortens fully-qualified types used in expression contexts (such as static method calls, static fields, and enum constants) while avoiding imports that would change how existing unqualified type references resolve. ([#3039](https://github.com/diffplug/spotless/pull/3039)) +- Eclipse JDT formatter step no longer fails with `NoClassDefFoundError` when lombok is active as a JVM agent (e.g. `-javaagent:lombok.jar` in Eclipse/VS Code/Cursor). ([#2795](https://github.com/diffplug/spotless/issues/2795)) + +## [3.10.1] - 2026-08-27 +### Fixed +- `` and other npm-based steps no longer fail to start on npm 12 (`EUNKNOWNCONFIG` from `--scripts-prepend-node-path`). ([#3024](https://github.com/diffplug/spotless/issues/3024)) + +## [3.10.0] - 2026-08-17 +### Added +- New `` step for Java, which replaces fully-qualified type names with their simple names and adds the imports they need. Best combined with `` and ``. ([#2945](https://github.com/diffplug/spotless/issues/2945)) +- Add embedded lockfiles to Eclipse JDT for every supported version (`4.9` through `4.40`), so `eclipse()` resolves from Maven Central instead of querying a P2 update site. Versions without an embedded lockfile still fall back to P2 provisioning. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +- Add support to apply alternate license header within same format ([#872](https://github.com/diffplug/spotless/issues/872)) +- Add support to skip license header application based on source file content pattern ([#650](https://github.com/diffplug/spotless/issues/650)). +### Fixed +- `removeUnusedImports` no longer fails on Java `import module` declarations. ([#2890](https://github.com/diffplug/spotless/issues/2890)) +- Concurrent P2 provisioning no longer races Solstice's on-disk cache (affects Eclipse-based formatters under parallel builds). ([#3004](https://github.com/diffplug/spotless/issues/3004)) +### Changes +- Default `google-java-format` remains `1.28.0` on JVM 17; bumps to `1.30.0` on JVM 21+; require at least `1.30.0` on JVM 25+ for `import module` support. +- Bump default `eclipse` version to latest `4.39` -> `4.40`. ([#1996](https://github.com/diffplug/spotless/issues/1996)) +- Document Maven skip properties `spotless.skip`, `spotless.check.skip`, and `spotless.apply.skip`. Goal-specific skips now live on their own mojos so they no longer leak across goals. ([#3009](https://github.com/diffplug/spotless/pull/3009)) +- Bump default `adocfmt` version `0.2.0` -> `0.3.1`, which adds table formatting support (``, ``, ``, ``). + +## [3.9.0] - 2026-07-27 +### Added +- Add support for Java formatting via [`prince-of-space`](https://github.com/agustafson/prince-of-space) with the new `` step. ([#2991](https://github.com/diffplug/spotless/pull/2991)) +### Changes +- Replace RDF formatter library `de.atextor:turtle-formatter` (discontinued) with `cool.rdf:cool-rdf-formatter` (its new coordinates); the RDF/turtle formatter now requires Java 25+. ([#2995](https://github.com/diffplug/spotless/pull/2995)) +- Bump default `greclipse` version to latest `4.39` -> `4.40`. ([#2989](https://github.com/diffplug/spotless/pull/2989)) +- Bump default `tabletest-formatter` version `1.1.1` -> `1.1.2`. + ## [3.8.0] - 2026-06-29 ### Added - Add support for custom string format for license header copyright year via `yearStringFormat()`. ([#2965](https://github.com/diffplug/spotless/pull/2965)) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 1d0bf6d25f..de25140289 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -2,18 +2,18 @@ [![MavenCentral](https://img.shields.io/badge/mavencentral-com.diffplug.spotless%3Aspotless--maven--plugin-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-maven-plugin%22) -[![Changelog](https://img.shields.io/badge/changelog-3.8.0-blue.svg)](CHANGES.md) -[![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-maven-plugin/3.8.0/index.html) +[![Changelog](https://img.shields.io/badge/changelog-3.10.4-blue.svg)](CHANGES.md) +[![Javadoc](https://img.shields.io/badge/javadoc-here-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-maven-plugin/3.10.4/index.html) Spotless is a general-purpose formatting plugin used by [6,000 projects on GitHub (Jan 2023)](https://github.com/search?l=Maven+POM&q=spotless&type=Code). It is completely ร  la carte, but also includes powerful "batteries-included" if you opt-in. Plugin requires a version of Maven higher or equal to 3.1.0. @@ -40,7 +40,7 @@ user@machine repo % mvn spotless:check - [Git hook (optional)](#git-hook) - [Binding to maven phase](#binding-to-maven-phase) - **Languages** - - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) + - [Java](#java) ([removeUnusedImports](#removeunusedimports), [forbidWildcardImports](#forbidwildcardimports), [expandWildcardImports](#expandwildcardimports), [forbidModuleImports](#forbidmoduleimports), [shortenFullyQualifiedTypes](#shortenfullyqualifiedtypes), [google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [prince-of-space](#prince-of-space), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [tabletest-formatter](#tabletest-formatter-1), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) @@ -234,8 +234,9 @@ any other maven phase (i.e. compile) then it can be configured as below; - + + @@ -260,12 +261,79 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` +### expandWildcardImports + +This step expands all wildcard imports to single class imports. +To do this, [JavaParser](https://javaparser.org/) is used to parse the complete sourcecode and resolve the full qualified name of all used classes and static methods from the full classpath. +This operation can be resource intensive when formatting many source files, so you may want to change to `forbidWildcardImports` when your codebase is cleaned and stable. + +```xml + +``` + ### forbidModuleImports ```xml ``` +### shortenFullyQualifiedTypes + +Replaces fully-qualified type names with their simple names, adding the imports they need. Useful for cleaning up generated code, or code where inline fully-qualified names have crept in. + +[JavaParser](https://javaparser.org/) parses the source and only type positions in the AST are rewritten, so fully-qualified names appearing in strings, text blocks, and comments are left alone. Unlike [`expandWildcardImports`](#expandwildcardimports), it works from the source file alone and does not need your compile classpath. + +New imports are appended after the existing ones, so run this before ``: + +```xml + + + +``` + +Before: + +```java +package com.acme; + +public class UserService { + private final java.util.Map> cache = new java.util.HashMap<>(); + + public java.util.List getUsers(java.util.function.Predicate filter) throws java.io.IOException { + return new java.util.ArrayList<>(); + } +} +``` + +After: + +```java +package com.acme; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class UserService { + private final Map> cache = new HashMap<>(); + + public List getUsers(Predicate filter) throws IOException { + return new ArrayList<>(); + } +} +``` + +A name is left fully-qualified whenever shortening it could change what the code means: + +- two different fully-qualified names in the file would collapse to the same simple name (e.g. `java.util.List` and `java.awt.List`) +- an existing import already binds that simple name to a different type +- the file does not parse + +Types in `java.lang` and in the file's own package are shortened without adding an import. + ### google-java-format [homepage](https://github.com/google/google-java-format). [changelog](https://github.com/google/google-java-format/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java). @@ -276,11 +344,14 @@ any other maven phase (i.e. compile) then it can be configured as below; true false + false. com.google.googlejavaformat:google-java-format ``` +Note: when enabling `AOSP` style, `reorderImports` needs to be enabled to apply the correct import order from [the AOSP guidelines](https://source.android.com/docs/setup/contribute/code-style#order-import-statements). + ### palantir-java-format [homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java). @@ -293,6 +364,23 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` +### prince-of-space + +[homepage](https://github.com/agustafson/prince-of-space). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PrinceOfSpace.java). Requires a JDK 17+ host runtime. + +```xml + + 2.2.0 + SPACES + 4 + 120 + BALANCED + true + false + 17 + +``` + ### eclipse jdt [homepage](https://download.eclipse.org/eclipse/downloads/). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java). See [here](../ECLIPSE_SCREENSHOTS.md) for screenshots that demonstrate how to get and install the config file mentioned below. @@ -409,7 +497,7 @@ These mechanisms already exist for the Gradle plugin. ```xml - 1.1.1 + 1.1.2 ``` @@ -487,7 +575,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ### ktfmt -[homepage](https://github.com/facebook/ktfmt). [changelog](https://github.com/facebook/ktfmt/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java). +[homepage](https://github.com/Kotlin/ktfmt). [changelog](https://github.com/Kotlin/ktfmt/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java). ```xml @@ -545,7 +633,7 @@ Additionally, `editorConfigOverride` options will override what's supplied in `. ```xml - 1.1.1 + 1.1.2 ``` @@ -727,7 +815,7 @@ Additionally, `editorConfigOverride` options will override what's supplied in `. ```xml - 0.2.0 + 0.3.1 true true true @@ -739,6 +827,10 @@ Additionally, `editorConfigOverride` options will override what's supplied in `. false true false + true + AUTO + 120 + ALL ``` @@ -1247,7 +1339,7 @@ When formatting shell scripts via `shfmt`, configure `shfmt` settings via `.edit src/**/*.table - 1.1.1 + 1.1.2 @@ -1320,17 +1412,19 @@ List of generic configuration `parameters (type/default)` the build fails for any of them. You can ignore warnings using this parameter. They will still be logged in the plugin's output. * `verify (boolean/true)`: If `true`, the content before and after formatting is parsed to an RDF model and compared for isomorphicity. -* `turtleFormatterVersion (string|RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION)`: the version of turtle-formatter to use (see below). +* `turtleFormatterVersion (string|RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION)`: the version of Cool RDF Formatter to use (see below). ### Supported RDF formats: only TTL (at the moment) -Formatting TTL is done using [turtle-formatter](https://github.com/atextor/turtle-formatter), -which is highly configurable (have a look at the [Style Documentation](https://github.com/atextor/turtle-formatter?tab=readme-ov-file#customizing-the-style)) +Formatting TTL is done using [Cool RDF Formatter](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter), +which is highly configurable (have a look at the [Style Documentation](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter)) and will handle blank nodes the way you'd hope. -The style options can be configured via spotless. Wherever the style wants a URI (for example, for the `predicateOrder`, you can -use the abbreviated form if it is a `FormattingStyle.KnownPrefix` (currently `rdf`, `rdfs`, `xsd`, `owl`, `dcterms`) -Error messages will give you hints. To configure the TTL formatting style, pass the configuration parameters under `` +The style options can be configured via spotless. Wherever the style wants a URI (for example, for the `predicateOrder`, you can +use the abbreviated form if it is a known `RdfPrefix` from Cool RDF's `Prefixes` enum. +Error messages will give you hints. To configure the TTL formatting style, pass the configuration parameters under ``. +For example, Cool RDF Formatter versions which support `preserveBlankNodeLabelsAndOrdering` (default true) can set it with +`false`.Set it to `false`, to use Cool RDF's default behavior (stable blank node ordering). ### Examples Minimal: @@ -1354,10 +1448,11 @@ Configuring some generic and TTL options: false false - 1.2.13 + 2.0.0 RIGHT true + false @@ -1366,7 +1461,7 @@ Configuring some generic and TTL options: ### Libraries and versions RDF parsing is done via [Apache Jena](https://jena.apache.org/) in the version that -[turtle-formatter](https://github.com/atextor/turtle-formatter) depends on (not necessarily the latest). +[Cool RDF Formatter](https://github.com/cool-rdf/cool-rdf/tree/main/cool-rdf-formatter) depends on (not necessarily the latest). ## Protobuf @@ -1933,6 +2028,25 @@ Some files have fixed header lines (e.g. `^#!.+?$` to skip shebangs). +### Skip headers and multiple license headers + +Sometimes it may be necessary to disable a license rule for specific files, or to maintain dual licenses. + +To define alternate replacements: + +```xml + + PrimaryHeaderLicense + /** Base License Header */ + Best + + + SecondaryHeaderLicense + /** Alternate License Header */ + .*Test.+ + +``` + @@ -2051,12 +2165,23 @@ You can easily set the line endings of different files using [a `.gitattributes` -## Disabling warnings and error messages +## Disabling Spotless goals + +By default, `spotless:check` is bound to the `verify` phase. You might want to disable Spotless for some builds. We [recommend against this](https://github.com/diffplug/spotless/issues/79#issuecomment-290844602), but the following properties are available: + +| Property | Scope | Effect | +| --- | --- | --- | +| `spotless.skip` | `spotless:check` and `spotless:apply` | Skips both formatting goals | +| `spotless.check.skip` | `spotless:check` only | Skips only the check goal | +| `spotless.apply.skip` | `spotless:apply` only | Skips only the apply goal | + +You can set them at the command line or in the `` section of the `pom.xml`: -By default, `spotless:check` is bound to the `verify` phase. You might want to disable this behavior. We [recommend against this](https://github.com/diffplug/spotless/issues/79#issuecomment-290844602), but it's easy to do if you'd like: +- `-Dspotless.skip=true` / `true` โ€” skip both `spotless:check` and `spotless:apply` +- `-Dspotless.check.skip=true` / `true` โ€” skip only `spotless:check` (including when it is bound to `verify`) +- `-Dspotless.apply.skip=true` / `true` โ€” skip only `spotless:apply` -- set `-Dspotless.check.skip=true` at the command line -- set `spotless.check.skip` to `true` in the `` section of the `pom.xml` +`spotless.check.skip` does **not** skip `spotless:apply`, and `spotless.apply.skip` does **not** skip `spotless:check`. Use `spotless.skip` when you want both. These properties do **not** affect `spotless:install-git-pre-push-hook`. ### Suppressing lint errors diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle deleted file mode 100644 index 79d9108e80..0000000000 --- a/plugin-maven/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -import org.gradlex.maven.plugin.development.task.GenerateHelpMojoSourcesTask -import org.gradlex.maven.plugin.development.task.GenerateMavenPluginDescriptorTask - -plugins { - // https://github.com/gradlex-org/maven-plugin-development - alias libs.plugins.maven.plugin.development -} - -apply from: rootProject.file('gradle/changelog.gradle') -ext.artifactId = project.artifactIdMaven -version = spotlessChangelog.versionNext - -apply from: rootProject.file("gradle/java-setup.gradle") -apply from: rootProject.file('gradle/spotless-freshmark.gradle') - -mavenPlugin { - name = 'Spotless Maven Plugin' - artifactId = project.artifactIdMaven - description = project.description -} - -dependencies { - implementation projects.lib - implementation projects.libExtra - - compileOnly libs.maven.plugin.api - compileOnly libs.maven.plugin.annotations - compileOnly libs.maven.core - compileOnly libs.aether.api - - compileOnly libs.jakarta.annotation.api - - implementation libs.durian.core - implementation libs.durian.io - implementation libs.durian.collect - implementation libs.plexus.resources - implementation libs.plexus.utils - implementation libs.jgit - implementation libs.plexus.build.api - - testImplementation project(":testlib") - testImplementation libs.junit.jupiter - testImplementation libs.assertj.core - testImplementation libs.mockito.core - testImplementation libs.durian.io - testImplementation libs.mustache.compiler - testImplementation libs.owasp.encoder - testImplementation libs.maven.plugin.api - testImplementation libs.aether.api - testImplementation libs.plexus.resources - testImplementation libs.maven.core - testRuntimeOnly "org.junit.platform:junit-platform-launcher" -} - -apply from: rootProject.file('gradle/special-tests.gradle') -tasks.withType(Test).configureEach { - systemProperty 'spotlessMavenPluginVersion', project.version - systemProperty 'spotlessProjectDir', "${project.rootProject.projectDir}".toString() - dependsOn 'publishToMavenLocal' - dependsOn ':lib:publishToMavenLocal' - dependsOn ':lib-extra:publishToMavenLocal' -} - -apply from: rootProject.file("gradle/java-publish.gradle") diff --git a/plugin-maven/build.gradle.kts b/plugin-maven/build.gradle.kts new file mode 100644 index 0000000000..62511b936d --- /dev/null +++ b/plugin-maven/build.gradle.kts @@ -0,0 +1,75 @@ +plugins { + alias(libs.plugins.maven.plugin.development) + id("spotless.changelog") + id("spotless.java-setup") + id("spotless.spotless-freshmark") + id("spotless.special-tests") + id("spotless.java-publish") +} + +version = spotlessChangelog.versionNext + +mavenPlugin { + name = property("POM_NAME").toString() + artifactId = property("POM_ARTIFACT_ID").toString() + description = property("POM_DESCRIPTION").toString() +} + +mavenPublishing { + pom { + developers { + developer { + id = "lutovich" + name = "Konstantin Lutovich" + email = "konstantin.lutovich@neotechnology.com" + } + } + withXml { + val rootNode = asNode() + val prerequisites = rootNode.appendNode("prerequisites") + prerequisites.appendNode("maven", "3.1.0") + } + } +} + +dependencies { + implementation(projects.lib) + implementation(projects.libExtra) + + compileOnly(pinnedLibs.maven.plugin.api) + compileOnly(pinnedLibs.maven.plugin.annotations) + compileOnly(pinnedLibs.maven.core) + compileOnly(pinnedLibs.aether.api) + + compileOnly(libs.jakarta.annotation.api) + compileOnly(libs.javax.inject) + + implementation(libs.durian.core) + implementation(libs.durian.io) + implementation(libs.durian.collect) + implementation(libs.plexus.resources) + implementation(libs.plexus.utils) + implementation(libs.jgit) + implementation(pinnedLibs.plexus.build.api) + + testImplementation(projects.testlib) + testImplementation(libs.junit.jupiter) + testImplementation(libs.assertj.core) + testImplementation(libs.mockito.core) + testImplementation(libs.durian.io) + testImplementation(libs.mustache.compiler) + testImplementation(libs.owasp.encoder) + testImplementation(pinnedLibs.maven.plugin.api) + testImplementation(pinnedLibs.aether.api) + testImplementation(libs.plexus.resources) + testImplementation(pinnedLibs.maven.core) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType().configureEach { + systemProperty("spotlessMavenPluginVersion", project.version) + systemProperty("spotlessProjectDir", "${project.rootProject.projectDir}") + dependsOn(tasks.publishToMavenLocal) + dependsOn(":lib:publishToMavenLocal") + dependsOn(":lib-extra:publishToMavenLocal") +} diff --git a/plugin-maven/gradle.properties b/plugin-maven/gradle.properties new file mode 100644 index 0000000000..f0c5eeda09 --- /dev/null +++ b/plugin-maven/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=spotless-maven-plugin +POM_NAME=Spotless Maven Plugin diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index b744ddf418..58c876300d 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -40,10 +40,11 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import javax.inject.Inject; + import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.resource.ResourceManager; @@ -102,16 +103,16 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { static final String GOAL_APPLY = "apply"; static final String GOAL_PRE_PUSH_HOOK = "install-git-pre-push-hook"; - @Component + @Inject private RepositorySystem repositorySystem; - @Component + @Inject private ResourceManager resourceManager; - @Component + @Inject protected BuildContext buildContext; - @Component + @Parameter(defaultValue = "${session}", readonly = true) private MavenSession mavenSession; @Parameter(defaultValue = "${mojoExecution.goal}", required = true, readonly = true) @@ -120,12 +121,6 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter(property = "spotless.skip", defaultValue = "false") private boolean skip; - @Parameter(property = "spotless.apply.skip", defaultValue = "false") - private boolean applySkip; - - @Parameter(property = "spotless.check.skip", defaultValue = "false") - private boolean checkSkip; - @Parameter(defaultValue = "${project}", required = true, readonly = true) private MavenProject project; @@ -305,16 +300,14 @@ private boolean shouldSkip() { getLog().debug("Skipping for incremental builds as parameter 'enableForIncrementalBuilds' is set to 'false'"); return true; } + return isGoalSpecificSkip(); + } - switch (goal) { - case GOAL_CHECK: - return checkSkip; - case GOAL_APPLY: - return applySkip; - default: - break; - } - + /** + * Goal-specific skip flags live on the concrete mojos (e.g. {@code spotless.check.skip}). + * Override when a goal has its own property. + */ + protected boolean isGoalSpecificSkip() { return false; } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 1faf4b1c4e..ef77c7103b 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -47,6 +47,14 @@ public class SpotlessApplyMojo extends AbstractSpotlessMojo { @Parameter(property = "spotlessIdeHookUseStdOut") private boolean spotlessIdeHookUseStdOut; + @Parameter(property = "spotless.apply.skip", defaultValue = "false") + private boolean applySkip; + + @Override + protected boolean isGoalSpecificSkip() { + return applySkip; + } + @Override protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { if (isIdeHook()) { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index 910a004dfe..4638d58f44 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,9 @@ public class SpotlessCheckMojo extends AbstractSpotlessMojo { private static final String INCREMENTAL_MESSAGE_PREFIX = "Spotless Violation: "; + @Parameter(property = "spotless.check.skip", defaultValue = "false") + private boolean checkSkip; + public enum MessageSeverity { WARNING(BuildContext.SEVERITY_WARNING), ERROR(BuildContext.SEVERITY_ERROR); @@ -63,6 +66,11 @@ public int getSeverity() { @Parameter(defaultValue = "WARNING") private MessageSeverity m2eIncrementalBuildMessageSeverity; + @Override + protected boolean isGoalSpecificSkip() { + return checkSkip; + } + @Override protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { ImpactedFilesTracker counter = new ImpactedFilesTracker(); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java index 51362d3162..8a9b86ced8 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/asciidoc/Adocfmt.java @@ -60,6 +60,18 @@ public class Adocfmt implements FormatterStepFactory { @Parameter private Boolean ensureSourceDelimiters; + @Parameter + private Boolean formatTables; + + @Parameter + private String tableLayout; + + @Parameter + private Integer tableMaxLineWidth; + + @Parameter + private String tableBlankLines; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String version = this.version != null ? this.version : AdocfmtStep.defaultVersion(); @@ -86,6 +98,14 @@ public FormatterStep newFormatterStep(FormatterStepConfig config) { adocfmtConfig.ensureHeadingBlankLines = ensureHeadingBlankLines; if (ensureSourceDelimiters != null) adocfmtConfig.ensureSourceDelimiters = ensureSourceDelimiters; + if (formatTables != null) + adocfmtConfig.formatTables = formatTables; + if (tableLayout != null) + adocfmtConfig.tableLayout = tableLayout; + if (tableMaxLineWidth != null) + adocfmtConfig.tableMaxLineWidth = tableMaxLineWidth; + if (tableBlankLines != null) + adocfmtConfig.tableBlankLines = tableBlankLines; return AdocfmtStep.create(version, config.getProvisioner(), adocfmtConfig); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java index 617dfe328a..7b0a92a0ad 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/LicenseHeader.java @@ -28,6 +28,12 @@ public class LicenseHeader implements FormatterStepFactory { + @Parameter + private String name; + + @Parameter + private String onlyIfContentMatches; + @Parameter private String file; @@ -57,12 +63,17 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) { boolean updateYear = config.getRatchetFrom().isPresent(); yearMode = updateYear ? YearMode.UPDATE_TO_TODAY : YearMode.PRESERVE; } - return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString) + LicenseHeaderStep builder = LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString) .withYearMode(yearMode) .withSkipLinesMatching(skipLinesMatching) - .withYearStingFormat(yearStrFmt) - .build() - .filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter()); + .withYearStingFormat(yearStrFmt); + if (name != null) { + builder = builder.withName(name); + } + if (onlyIfContentMatches != null) { + builder = builder.withContentPattern(onlyIfContentMatches); + } + return builder.build().filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter()); } else { throw new IllegalArgumentException("Must specify exactly one of 'file' or 'content'."); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java index fe6e2633ac..996dbefea7 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java @@ -71,6 +71,10 @@ public void addPalantirJavaFormat(PalantirJavaFormat palantirJavaFormat) { addStepFactory(palantirJavaFormat); } + public void addPrinceOfSpace(PrinceOfSpace princeOfSpace) { + addStepFactory(princeOfSpace); + } + public void addRemoveUnusedImports(RemoveUnusedImports removeUnusedImports) { addStepFactory(removeUnusedImports); } @@ -99,6 +103,10 @@ public void addTableTestFormatter(TableTestFormatter tableTestFormatter) { addStepFactory(tableTestFormatter); } + public void addShortenFullyQualifiedTypes(ShortenFullyQualifiedTypes shortenFullyQualifiedTypes) { + addStepFactory(shortenFullyQualifiedTypes); + } + private static String fileMask(Path path) { String dir = path.toString(); if (!dir.endsWith(File.separator)) { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PrinceOfSpace.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PrinceOfSpace.java new file mode 100644 index 0000000000..a862330ad7 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PrinceOfSpace.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.java; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.PrinceOfSpaceStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class PrinceOfSpace implements FormatterStepFactory { + + @Parameter + private String version; + + @Parameter + private String indentStyle; + + @Parameter + private Integer indentSize; + + @Parameter + private Integer lineLength; + + @Parameter + private String wrapStyle; + + @Parameter + private Boolean closingParenOnNewLine; + + @Parameter + private Boolean trailingCommas; + + @Parameter + private Integer javaLanguageLevel; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + String version = this.version != null ? this.version : PrinceOfSpaceStep.defaultVersion(); + PrinceOfSpaceStep.Options options = new PrinceOfSpaceStep.Options(); + if (indentStyle != null) { + options.setIndentStyle(indentStyle); + } + if (indentSize != null) { + options.setIndentSize(indentSize); + } + if (lineLength != null) { + options.setLineLength(lineLength); + } + if (wrapStyle != null) { + options.setWrapStyle(wrapStyle); + } + if (closingParenOnNewLine != null) { + options.setClosingParenOnNewLine(closingParenOnNewLine); + } + if (trailingCommas != null) { + options.setTrailingCommas(trailingCommas); + } + if (javaLanguageLevel != null) { + options.setJavaLanguageLevel(javaLanguageLevel); + } + return PrinceOfSpaceStep.create(version, config.getProvisioner(), options); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ShortenFullyQualifiedTypes.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ShortenFullyQualifiedTypes.java new file mode 100644 index 0000000000..83286608e1 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ShortenFullyQualifiedTypes.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.java; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.ShortenFullyQualifiedTypesStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class ShortenFullyQualifiedTypes implements FormatterStepFactory { + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + return ShortenFullyQualifiedTypesStep.create(config.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JsonPatch.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JsonPatch.java index a822ad09e2..467f5faf84 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JsonPatch.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JsonPatch.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,10 +26,8 @@ * A {@link FormatterStepFactory} implementation that corresponds to {@code ...} configuration element. */ public class JsonPatch implements FormatterStepFactory { - private static final String DEFAULT_ZJSONPATCH_VERSION = "0.4.14"; - @Parameter - String zjsonPatchVersion = DEFAULT_ZJSONPATCH_VERSION; + String zjsonPatchVersion = JsonPatchStep.defaultVersion(); @Parameter String patch; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/FormatterStepFactoryTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/FormatterStepFactoryTest.java index ff98996431..f2b5a2f31c 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/FormatterStepFactoryTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/FormatterStepFactoryTest.java @@ -24,9 +24,11 @@ import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.aether.RepositorySystemSession; @@ -51,11 +53,6 @@ void defaultP2CacheDirectoryUsesMavenLocalRepository() { .isEqualTo(new File(localRepository, "dev/equo/p2-data")); } - @Test - void eclipseUsesConfiguredCacheDirectory() throws Exception { - assertUsesConfiguredCacheDirectory(new Eclipse()); - } - @Test void grEclipseUsesConfiguredCacheDirectory() throws Exception { assertUsesConfiguredCacheDirectory(new GrEclipse()); @@ -66,11 +63,6 @@ void eclipseCdtUsesConfiguredCacheDirectory() throws Exception { assertUsesConfiguredCacheDirectory(new EclipseCdt()); } - @Test - void eclipseUsesDefaultCacheDirectory() throws Exception { - assertUsesDefaultCacheDirectory(new Eclipse()); - } - @Test void grEclipseUsesDefaultCacheDirectory() throws Exception { assertUsesDefaultCacheDirectory(new GrEclipse()); @@ -81,6 +73,44 @@ void eclipseCdtUsesDefaultCacheDirectory() throws Exception { assertUsesDefaultCacheDirectory(new EclipseCdt()); } + /** + * Eclipse JDT ships an embedded lockfile for its default version, so it resolves straight from + * Maven and never runs a P2 query -- which also means its P2 cache directory goes unused. The + * P2 cache directory behavior is covered by the grEclipse and eclipseCdt cases above, neither + * of which has an embedded lockfile. + */ + @Test + void eclipseResolvesFromEmbeddedLockfileInsteadOfP2() throws Exception { + Eclipse factory = new Eclipse(); + factory.init(repositorySystemSession(tempDir.resolve("local-repo").toFile())); + + AtomicReference> actualCoordinates = new AtomicReference<>(); + AtomicBoolean p2WasQueried = new AtomicBoolean(); + File fakeJar = tempDir.resolve("fake.jar").toFile(); + Files.write(fakeJar.toPath(), new byte[]{0}); + + FormatterStep step = factory.newFormatterStep(new FormatterStepConfig( + UTF_8, + "", + Optional.empty(), + (withTransitives, mavenCoordinates) -> { + actualCoordinates.set(mavenCoordinates); + return Set.of(fakeJar); + }, + (modelWrapper, mavenProvisioner, cacheDirectory) -> { + p2WasQueried.set(true); + return List.of(fakeJar); + }, + null, + Optional.empty(), + Optional.empty())); + + int unused = step.hashCode(); + + assertThat(p2WasQueried).isFalse(); + assertThat(actualCoordinates.get()).anyMatch(coordinate -> coordinate.startsWith("org.eclipse.jdt:org.eclipse.jdt.core:")); + } + private void assertUsesConfiguredCacheDirectory(FormatterStepFactory factory) throws Exception { File configuredCacheDirectory = tempDir.resolve("configured-p2-cache").toFile(); Field cacheDirectory = cacheDirectoryField(factory); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java index faedaad602..56bb2e666e 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java @@ -88,7 +88,7 @@ void singleProjectExhaustiveGlobal() throws Exception { private void checkBehavior(Git git) throws Exception { setFile(TEST_PATH).toContent("HELLO"); git.add().addFilepattern(TEST_PATH).call(); - git.commit().setMessage("Initial state").call(); + git.commit().setSign(false).setMessage("Initial state").call(); // tag this initial state as the baseline for spotless to ratchet from git.tag().setName("baseline").call(); @@ -165,7 +165,7 @@ void worktreeSupport() throws Exception { setFile(TEST_PATH).toContent("HELLO"); mainGit.add().addFilepattern(TEST_PATH).call(); - mainGit.commit().setMessage("Initial commit").call(); + mainGit.commit().setSign(false).setMessage("Initial commit").call(); mainGit.tag().setName("baseline").call(); // Set up a worktree manually (JGit doesn't support worktrees) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java index 92a78e3923..f848149f8b 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ class SpotlessCheckMojoTest extends MavenIntegrationHarness { private static final String UNFORMATTED_FILE = "license/MissingLicense.test"; private static final String FORMATTED_FILE = "license/HasLicense.test"; + private static final String TARGET_JAVA = "src/main/java/com.github.youribonnaffe.gradle.format/Java8Test.java"; @Test void testSpotlessCheckWithFormattingViolations() throws Exception { @@ -46,6 +47,19 @@ void testSkipSpotlessCheckWithFormattingViolations() throws Exception { testSpotlessCheck(UNFORMATTED_FILE, "spotless:check -Dspotless.check.skip", false); } + @Test + void testSkipAllGoalsWithSpotlessSkip() throws Exception { + writePomWithJavaLicenseHeaderStep(); + testSpotlessCheck(UNFORMATTED_FILE, "spotless:check -Dspotless.skip", false); + } + + @Test + void testApplySkipDoesNotSkipCheck() throws Exception { + writePomWithJavaLicenseHeaderStep(); + // apply.skip must not suppress check + testSpotlessCheck(UNFORMATTED_FILE, "spotless:check -Dspotless.apply.skip", true); + } + @Test void testSpotlessCheckBindingToVerifyPhase() throws Exception { writePom( @@ -68,9 +82,39 @@ void testSpotlessCheckBindingToVerifyPhase() throws Exception { testSpotlessCheck(UNFORMATTED_FILE, "verify", true); } + @Test + void testApplySkipLeavesFileUnformatted() throws Exception { + writePomWithJavaLicenseHeaderStep(); + setFile("license.txt").toResource("license/TestLicense"); + setFile(TARGET_JAVA).toResource(UNFORMATTED_FILE); + + mavenRunner().withArguments("spotless:apply -Dspotless.apply.skip").runNoError(); + assertFile(TARGET_JAVA).sameAsResource(UNFORMATTED_FILE); + } + + @Test + void testSpotlessSkipLeavesApplyUnformatted() throws Exception { + writePomWithJavaLicenseHeaderStep(); + setFile("license.txt").toResource("license/TestLicense"); + setFile(TARGET_JAVA).toResource(UNFORMATTED_FILE); + + mavenRunner().withArguments("spotless:apply -Dspotless.skip").runNoError(); + assertFile(TARGET_JAVA).sameAsResource(UNFORMATTED_FILE); + } + + @Test + void testCheckSkipDoesNotSkipApply() throws Exception { + writePomWithJavaLicenseHeaderStep(); + setFile("license.txt").toResource("license/TestLicense"); + setFile(TARGET_JAVA).toResource(UNFORMATTED_FILE); + + mavenRunner().withArguments("spotless:apply -Dspotless.check.skip").runNoError(); + assertFile(TARGET_JAVA).sameAsResource(FORMATTED_FILE); + } + private void testSpotlessCheck(String fileName, String command, boolean expectError) throws Exception { setFile("license.txt").toResource("license/TestLicense"); - setFile("src/main/java/com.github.youribonnaffe.gradle.format/Java8Test.java").toResource(fileName); + setFile(TARGET_JAVA).toResource(fileName); MavenRunner mavenRunner = mavenRunner().withArguments(command); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java index 87926e0be3..7bc03e32a8 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/asciidoc/AdocfmtMavenTest.java @@ -27,7 +27,7 @@ public class AdocfmtMavenTest extends MavenIntegrationHarness { public void missingIncludesFails() throws Exception { writePom(groupWithSteps("asciidoc", "", - " 0.2.0", + " 0.3.1", "")); ProcessRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError(); assertThat(result.stdOutUtf8()).contains("You must specify some files to include"); @@ -38,7 +38,7 @@ public void testAdocfmt() throws Exception { writePomWithAsciidocSteps( """ - 0.2.0 + 0.3.1 true true true @@ -50,6 +50,10 @@ public void testAdocfmt() throws Exception { true true true + true + AUTO + 120 + ALL """); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderRatchetTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderRatchetTest.java index 24200020f2..917c11688d 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderRatchetTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderRatchetTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 DiffPlug + * Copyright 2020-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ void normal() throws Exception { @Test void ratchetFrom() throws Exception { try (Git git = Git.init().setDirectory(rootFolder()).call()) { - git.commit().setMessage("First commit").call(); + git.commit().setSign(false).setMessage("First commit").call(); } setRatchetFrom("HEAD"); testSuiteUpdateWithLatest(true); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderTest.java index 92d74bc659..36c5c31527 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/LicenseHeaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ class LicenseHeaderTest extends MavenIntegrationHarness { private static final String KEY_LICENSE = "license/TestLicense"; private static final String KOTLIN_LICENSE_HEADER = "// Hello, I'm Kotlin license header"; + private static final String TEST_JAVA = "src/main/java/pkg/Test.java"; + private static final String CONTENT = "package pkg;\npublic class Test {}"; @Test void fromFileJava() throws Exception { @@ -33,6 +35,64 @@ void fromFileJava() throws Exception { runTest(); } + /** + * When {@code onlyIfContentMatches} matches the file content, the license header is applied. + */ + @Test + void onlyIfContentMatchesAppliesWhenPatternMatches() throws Exception { + writePomWithJavaSteps( + "", + " /** New License Header */", + " .+Test.+", + ""); + + setFile(TEST_JAVA).toContent(CONTENT); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_JAVA).hasContent("/** New License Header */\n" + CONTENT); + } + + /** + * When {@code onlyIfContentMatches} does not match the file content, the existing + * content (including any existing header) is left unchanged. + */ + @Test + void onlyIfContentMatchesSkipsWhenPatternDoesNotMatch() throws Exception { + writePomWithJavaSteps( + "", + " /** Should Not Be Applied */", + " missingString", + ""); + + String existing = "/** This license header should be preserved */\n" + CONTENT; + setFile(TEST_JAVA).toContent(existing); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_JAVA).hasContent(existing); + } + + /** + * Multiple named license-header steps with different {@code onlyIfContentMatches} + * patterns: the step whose pattern matches is applied (parity with Gradle + * {@code filterByContentPatternTest}). + */ + @Test + void multipleNamedLicenseHeadersSelectByContentPattern() throws Exception { + writePomWithJavaSteps( + "", + " PrimaryHeaderLicense", + " /** Base License Header */", + " Best", + "", + "", + " SecondaryHeaderLicense", + " /** Alternate License Header */", + " .*Test.+", + ""); + + setFile(TEST_JAVA).toContent("/** 2003 */\n" + CONTENT); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_JAVA).hasContent("/** Alternate License Header */\n" + CONTENT); + } + @Test void fromContentCpp() throws Exception { String cppLicense = "//my license"; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/GoogleJavaFormatTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/GoogleJavaFormatTest.java index 52e89e58db..0723d73c0e 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/GoogleJavaFormatTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/GoogleJavaFormatTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; +import com.diffplug.spotless.java.GoogleJavaFormatStep; import com.diffplug.spotless.maven.MavenIntegrationHarness; class GoogleJavaFormatTest extends MavenIntegrationHarness { @@ -24,7 +25,7 @@ class GoogleJavaFormatTest extends MavenIntegrationHarness { void specificVersionDefaultStyle() throws Exception { writePomWithJavaSteps( "", - " 1.17.0", + " " + GoogleJavaFormatStep.defaultVersion() + "", ""); runTest("java/googlejavaformat/JavaCodeFormatted.test"); @@ -34,7 +35,7 @@ void specificVersionDefaultStyle() throws Exception { void specificVersionSpecificStyle() throws Exception { writePomWithJavaSteps( "", - " 1.17.0", + " " + GoogleJavaFormatStep.defaultVersion() + "", " ", ""); @@ -45,7 +46,7 @@ void specificVersionSpecificStyle() throws Exception { void specificVersionReflowLongStrings() throws Exception { writePomWithJavaSteps( "", - " 1.17.0", + " " + GoogleJavaFormatStep.defaultVersion() + "", " true", ""); @@ -56,7 +57,7 @@ void specificVersionReflowLongStrings() throws Exception { void specificVersionReorderImports() throws Exception { writePomWithJavaSteps( "", - " 1.17.0", + " " + GoogleJavaFormatStep.defaultVersion() + "", " ", " true", ""); @@ -68,7 +69,7 @@ void specificVersionReorderImports() throws Exception { void specificVersionSkipJavadocFormatting() throws Exception { writePomWithJavaSteps( "", - " 1.17.0", + " " + GoogleJavaFormatStep.defaultVersion() + "", " false", ""); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/PalantirJavaFormatTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/PalantirJavaFormatTest.java index eee4fab8c5..ffac52c6af 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/PalantirJavaFormatTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/PalantirJavaFormatTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 DiffPlug + * Copyright 2022-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; +import com.diffplug.spotless.java.PalantirJavaFormatStep; import com.diffplug.spotless.maven.MavenIntegrationHarness; class PalantirJavaFormatTest extends MavenIntegrationHarness { @@ -34,7 +35,7 @@ void specificVersionDefaultStyle() throws Exception { void specificJava11Version2() throws Exception { writePomWithJavaSteps( "", - " 2.39.0", + " " + PalantirJavaFormatStep.defaultVersion() + "", ""); runTest("java/palantirjavaformat/JavaCodeFormatted.test", "java/palantirjavaformat/JavaCodeUnformatted.test"); @@ -44,7 +45,7 @@ void specificJava11Version2() throws Exception { void formatJavaDoc() throws Exception { writePomWithJavaSteps( "", - " 2.39.0", + " " + PalantirJavaFormatStep.defaultVersion() + "", " true", ""); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java index dc6e59fef0..3978715d95 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ void testDiktatWithVersion() throws Exception { writePomWithKotlinSteps( "", - " 1.2.1", + " 2.0.0", ""); String path = "src/main/kotlin/Main.kt"; @@ -56,7 +56,7 @@ void testDiktatConfig() throws Exception { File conf = setFile(configPath).toResource("kotlin/diktat/diktat-analysis.yml"); writePomWithKotlinSteps( "", - " 1.2.1", + " 2.0.0", " " + conf.getAbsolutePath() + "", ""); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java index 78830bf09d..dd840796b7 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public class NpmTestsWithDynamicallyInstalledNpmInstallationTest extends MavenIn @Test void useDownloadedNpmInstallation() throws Exception { writePomWithPrettierSteps( - pomPluginLines("v18.13.0", null), + pomPluginLines("v22.14.0", null), "src/main/typescript/test.ts", "", " " + installedNpmPath() + "", diff --git a/rewrite.yml b/rewrite.yml index aceb5d76be..6c43d58671 100644 --- a/rewrite.yml +++ b/rewrite.yml @@ -14,15 +14,15 @@ recipeList: - org.openrewrite.java.migrate.UpgradeToJava17 - org.openrewrite.java.migrate.lang.StringRulesRecipes - org.openrewrite.java.migrate.util.JavaLangAPIs - - org.openrewrite.java.migrate.util.JavaUtilAPIs + # - org.openrewrite.java.migrate.util.JavaUtilAPIs # MigrateCollectionsEmpty* swaps Collections.emptyList/Map/Set for List/Map/Set.of; Gradle's configuration cache cannot round-trip those, which breaks IsolatedProjectTest - org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose - org.openrewrite.java.migrate.util.ReplaceStreamCollectWithToList - org.openrewrite.java.migrate.util.SequencedCollection - org.openrewrite.java.recipes.JavaRecipeBestPractices - org.openrewrite.java.recipes.RecipeTestingBestPractices - org.openrewrite.staticanalysis.BufferedWriterCreationRecipes - - org.openrewrite.staticanalysis.CommonStaticAnalysis - - org.openrewrite.staticanalysis.EqualsAvoidsNull + # - org.openrewrite.staticanalysis.CommonStaticAnalysis # bundles OrderImports, which permanently disagrees with gradle/spotless.importorder (each tool undoes the other) + # - org.openrewrite.staticanalysis.EqualsAvoidsNull # wants to flip x.equals("literal") to "literal".equals(x) repo-wide; not worth the churn - org.openrewrite.staticanalysis.JavaApiBestPractices - org.openrewrite.staticanalysis.LowercasePackage - org.openrewrite.staticanalysis.MissingOverrideAnnotation @@ -36,16 +36,16 @@ recipeList: - org.openrewrite.staticanalysis.SimplifyTernaryRecipes - org.openrewrite.staticanalysis.URLEqualsHashCodeRecipes - org.openrewrite.staticanalysis.UnnecessaryCloseInTryWithResources - - org.openrewrite.staticanalysis.UnnecessaryExplicitTypeArguments + # - org.openrewrite.staticanalysis.UnnecessaryExplicitTypeArguments # strips the required witness from Jvm.support(..), producing code that does not compile - org.openrewrite.staticanalysis.UnnecessaryParentheses - org.openrewrite.staticanalysis.UnnecessaryReturnAsLastStatement - tech.picnic.errorprone.refasterrules.BigDecimalRulesRecipes - - tech.picnic.errorprone.refasterrules.CharSequenceRulesRecipes + # - tech.picnic.errorprone.refasterrules.CharSequenceRulesRecipes # length() == 0 -> isEmpty(); not worth the churn - tech.picnic.errorprone.refasterrules.ClassRulesRecipes # tech.picnic.errorprone.refasterrules.CollectionRulesRecipes # needs UpgradeToJava21 - tech.picnic.errorprone.refasterrules.ComparatorRulesRecipes - tech.picnic.errorprone.refasterrules.EqualityRulesRecipes - - tech.picnic.errorprone.refasterrules.FileRulesRecipes + # - tech.picnic.errorprone.refasterrules.FileRulesRecipes # FilesNewOutputStreamToPath rewrites new FileOutputStream(..) to Files.newOutputStream(..), which does not compile where the variable is declared FileOutputStream - tech.picnic.errorprone.refasterrules.MapRulesRecipes - tech.picnic.errorprone.refasterrules.MicrometerRulesRecipes - tech.picnic.errorprone.refasterrules.MockitoRulesRecipes @@ -55,6 +55,6 @@ recipeList: - tech.picnic.errorprone.refasterrules.PreconditionsRulesRecipes - tech.picnic.errorprone.refasterrules.PrimitiveRulesRecipes - tech.picnic.errorprone.refasterrules.StreamRulesRecipes - - tech.picnic.errorprone.refasterrules.StringRulesRecipes + # - tech.picnic.errorprone.refasterrules.StringRulesRecipes # String.format -> "..".formatted and friends; not worth the churn - tech.picnic.errorprone.refasterrules.TimeRulesRecipes --- diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 8b529d91de..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,86 +0,0 @@ -import static java.lang.System.getenv - -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id 'com.diffplug.spotless' version '8.7.0' apply false - id 'com.github.spotbugs' version '6.5.8' apply false // https://github.com/spotbugs/spotbugs-gradle-plugin/releases - id 'com.gradle.develocity' version '4.4.3' -} - -dependencyResolutionManagement { - repositories { - mavenCentral() - } -} - -if (System.env['CI'] != null) { - // use the remote buildcache on all CI builds - buildCache { - def cred = { - if (System.env[it] != null) { - return System.env[it] - } else { - return System.getProperty(it) - } - } - remote(HttpBuildCache) { - url = 'https://buildcache.diffplug.com/cache/' - // but we only push if it's a trusted build (not PRs) - String user = cred('buildcacheuser') - String pass = cred('buildcachepass') - if (user != null && pass != null) { - push = true - credentials { - username = user - password = pass - } - } else { - credentials { username = 'anonymous' } - } - } - } -} - -develocity { - buildScan { - termsOfUseUrl = "https://gradle.com/terms-of-service" - termsOfUseAgree = "yes" - publishing { - onlyIf { getenv('CI') != null } - } - } -} - -enableFeaturePreview("STABLE_CONFIGURATION_CACHE") -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -rootProject.name = 'spotless' - -include 'lib' // reusable library with no dependencies -include 'lib-extra' // reusable library with lots of dependencies -include 'plugin-gradle' // gradle-specific glue code -include 'testlib' // library for sharing test infrastructure between the projects below - -def getStartProperty(String name) { - def value = startParameter.getProjectProperties().get(name) - if(null != value) { - return value - } - // user properties are not available in the startParameter - def userPropertiesFile = new File(startParameter.getGradleUserHomeDir(), 'gradle.properties') - def userProperties = new Properties() - if (userPropertiesFile.exists()) { - userProperties.load(userPropertiesFile.newReader()) - } - return userProperties.get(name) -} - -if (!getenv('SPOTLESS_EXCLUDE_MAVEN')?.toBoolean() && !Boolean.valueOf(getStartProperty('SPOTLESS_EXCLUDE_MAVEN') as String)) { - include 'plugin-maven' // maven-specific glue code -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..9bdbf29868 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,92 @@ +pluginManagement { + includeBuild("build-logic") + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("com.diffplug.spotless") version "8.10.3" apply false + id("com.github.spotbugs") version "6.5.12" apply false + id("com.gradle.develocity") version "4.6.0" +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("pinnedLibs") { + from(files("gradle/pinned-libs.versions.toml")) + } + } +} + +if (System.getenv("CI") != null) { + // use the remote buildcache on all CI builds + buildCache { + fun cred(name: String): String? = System.getenv(name) ?: System.getProperty(name) + remote { + url = uri("https://buildcache.diffplug.com/cache/") + // but we only push if it's a trusted build (not PRs) + val user = cred("buildcacheuser") + val pass = cred("buildcachepass") + if (user != null && pass != null) { + isPush = true + credentials { + username = user + password = pass + } + } else { + credentials { + username = "anonymous" + } + } + } + } +} + +develocity { + buildScan { + termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseAgree = "yes" + publishing { + onlyIf { System.getenv("CI") != null } + } + } +} + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +rootProject.name = "spotless" + +include("lib") // reusable library with no dependencies + +include("lib-extra") // reusable library with lots of dependencies + +include("plugin-gradle") // gradle-specific glue code + +include("testlib") // library for sharing test infrastructure between the projects below + +fun getStartProperty(name: String): String? = + startParameter.projectProperties[name] + ?: File(startParameter.gradleUserHomeDir, "gradle.properties") + .takeIf { it.exists() } + ?.reader() + ?.use { java.util.Properties().apply { load(it) }.getProperty(name) } + +// Groovy's String.toBoolean() -- what this used before the Kotlin DSL migration -- accepts "true", +// "y" and "1", ignoring case. Kotlin's toBoolean() only accepts "true", so the short forms would +// silently become no-ops. +fun String.toBooleanGroovy(): Boolean = trim().lowercase() in setOf("true", "y", "1") + +val excludeMaven = + System.getenv("SPOTLESS_EXCLUDE_MAVEN")?.toBooleanGroovy() == true || + getStartProperty("SPOTLESS_EXCLUDE_MAVEN")?.toBooleanGroovy() == true + +if (!excludeMaven) { + include("plugin-maven") // maven-specific glue code +} diff --git a/testlib/build.gradle b/testlib/build.gradle deleted file mode 100644 index 3ba4f011a5..0000000000 --- a/testlib/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id 'java-library' -} -ext.artifactId = project.artifactIdTestLib -version = rootProject.spotlessChangelog.versionNext -apply from: rootProject.file('gradle/java-setup.gradle') - -dependencies { - api projects.lib - api files(project(projects.lib.path).sourceSets.sortPom.output.classesDirs) - api libs.durian.core - api libs.durian.testlib - api libs.junit.jupiter - api libs.assertj.core - api libs.mockito.core - api libs.selfie.lib - api libs.selfie.runner.junit5 - runtimeOnly "org.junit.platform:junit-platform-launcher" - - implementation libs.durian.io - implementation libs.durian.collect - implementation libs.jgit - implementation gradleTestKit() -} - -// we'll hold the testlib to a low standard (prize brevity) -spotbugs { - // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). - reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH') -} - -apply from: rootProject.file('gradle/special-tests.gradle') -tasks.withType(Test).configureEach { - // for Antlr4FormatterStepTest, KtfmtStepTest, and KtLintStepTest - jvmArgs( - '--add-opens=java.base/java.lang=ALL-UNNAMED', - '--add-opens=java.base/java.util=ALL-UNNAMED', - ) -} - -javadoc { - options.addStringOption('Xdoclint:none', '-quiet') - options.addStringOption('Xwerror', '-quiet') -} diff --git a/testlib/build.gradle.kts b/testlib/build.gradle.kts new file mode 100644 index 0000000000..68f37a0d93 --- /dev/null +++ b/testlib/build.gradle.kts @@ -0,0 +1,50 @@ +import com.github.spotbugs.snom.Confidence + +plugins { + `java-library` + id("spotless.java-setup") + id("spotless.special-tests") +} + +version = spotlessChangelog.versionNext + +dependencies { + api(projects.lib) + api(libs.durian.core) + api(libs.durian.testlib) + api(libs.junit.jupiter) + api(libs.assertj.core) + api(libs.mockito.core) + api(libs.selfie.lib) + api(libs.selfie.runner.junit5) + runtimeOnly("org.junit.platform:junit-platform-launcher") + + implementation(libs.durian.io) + implementation(libs.durian.collect) + implementation(libs.jgit) + implementation(gradleTestKit()) +} + +// we'll hold the testlib to a low standard (prize brevity) +spotbugs { + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = Confidence.HIGH +} + +tasks.withType().configureEach { + // for Antlr4FormatterStepTest, KtfmtStepTest, and KtLintStepTest + jvmArgs( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + ) +} + +tasks.withType().configureEach { + options { + (this as? StandardJavadocDocletOptions)?.apply { + encoding = Charsets.UTF_8.name() + addStringOption("Xdoclint:none", "-quiet") + addStringOption("Xwerror", "-quiet") + } + } +} diff --git a/testlib/gradle.properties b/testlib/gradle.properties new file mode 100644 index 0000000000..e319d0a4cb --- /dev/null +++ b/testlib/gradle.properties @@ -0,0 +1,2 @@ +POM_ARTIFACT_ID=spotless-testlib +POM_NAME=spotless-testlib diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test new file mode 100644 index 0000000000..a5809f59da --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardFormatted.test @@ -0,0 +1,7 @@ +package test; + +import java.util.List; + +public class Test { + private List items; +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test new file mode 100644 index 0000000000..6d355e13bb --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JavaUtilWildcardUnformatted.test @@ -0,0 +1,7 @@ +package test; + +import java.util.*; + +public class Test { + private List items; +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test new file mode 100644 index 0000000000..bfdf5f36c6 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesFormatted.test @@ -0,0 +1,10 @@ +package test; + +import java.io.StringReader; +import org.xml.sax.InputSource; + +public class Test { + InputSource inputSource(String value) { + return new InputSource(new StringReader(value)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test new file mode 100644 index 0000000000..cfb2e355d7 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/JdkXmlTypesUnformatted.test @@ -0,0 +1,10 @@ +package test; + +import java.io.*; +import org.xml.sax.InputSource; + +public class Test { + InputSource inputSource(String value) { + return new InputSource(new StringReader(value)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test new file mode 100644 index 0000000000..fb5ae428c1 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesFormatted.test @@ -0,0 +1,18 @@ +package test; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class Test { + List sorted(List values) { + Collections.sort(values); + var iterator = values.iterator(); + values.forEach(value -> System.out.println(value.length())); + return iterator.hasNext() ? values : Collections.emptyList(); + } + + long timeoutMillis() { + return TimeUnit.SECONDS.toMillis(1); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test new file mode 100644 index 0000000000..3ef809fb6d --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/QualifierTypesUnformatted.test @@ -0,0 +1,17 @@ +package test; + +import java.util.*; +import java.util.concurrent.*; + +public class Test { + List sorted(List values) { + Collections.sort(values); + var iterator = values.iterator(); + values.forEach(value -> System.out.println(value.length())); + return iterator.hasNext() ? values : Collections.emptyList(); + } + + long timeoutMillis() { + return TimeUnit.SECONDS.toMillis(1); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test new file mode 100644 index 0000000000..301ef94e42 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test @@ -0,0 +1,15 @@ +package test; + +import static java.lang.Math.PI; +import static java.lang.Math.max; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class Test { + double circleArea(double radius) { + return PI * radius * radius; + } + + long timeoutMillis() { + return SECONDS.toMillis(max(1, 2)); + } +} diff --git a/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test new file mode 100644 index 0000000000..e4448ce810 --- /dev/null +++ b/testlib/src/main/resources/java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test @@ -0,0 +1,14 @@ +package test; + +import static java.lang.Math.*; +import static java.util.concurrent.TimeUnit.*; + +public class Test { + double circleArea(double radius) { + return PI * radius * radius; + } + + long timeoutMillis() { + return SECONDS.toMillis(max(1, 2)); + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/ModuleImportFormatted.test b/testlib/src/main/resources/java/removeunusedimports/ModuleImportFormatted.test new file mode 100644 index 0000000000..86ec1ef298 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/ModuleImportFormatted.test @@ -0,0 +1,10 @@ +import module java.base; +import java.util.ArrayList; +import java.util.List; + +class ModuleImportTest { + void test() { + UUID.randomUUID(); + List list = new ArrayList<>(); + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/ModuleImportUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/ModuleImportUnformatted.test new file mode 100644 index 0000000000..86ec1ef298 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/ModuleImportUnformatted.test @@ -0,0 +1,10 @@ +import module java.base; +import java.util.ArrayList; +import java.util.List; + +class ModuleImportTest { + void test() { + UUID.randomUUID(); + List list = new ArrayList<>(); + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedFormatted.test b/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedFormatted.test new file mode 100644 index 0000000000..b668944532 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedFormatted.test @@ -0,0 +1,10 @@ +import module java.base; +import java.util.ArrayList; +import java.util.List; + +class ModuleImportWithUnused { + void test() { + UUID.randomUUID(); + List list = new ArrayList<>(); + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedUnformatted.test new file mode 100644 index 0000000000..09da6d39f9 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/ModuleImportWithUnusedUnformatted.test @@ -0,0 +1,11 @@ +import module java.base; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +class ModuleImportWithUnused { + void test() { + UUID.randomUUID(); + List list = new ArrayList<>(); + } +} diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl-shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v2.0.0-default/shacl-shacl.ttl similarity index 100% rename from testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl-shacl.ttl rename to testlib/src/main/resources/rdf/ttl/expected/v2.0.0-default/shacl-shacl.ttl diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v2.0.0-default/shacl.ttl similarity index 100% rename from testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl.ttl rename to testlib/src/main/resources/rdf/ttl/expected/v2.0.0-default/shacl.ttl diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl-shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v2.0.0-style01/shacl-shacl.ttl similarity index 100% rename from testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl-shacl.ttl rename to testlib/src/main/resources/rdf/ttl/expected/v2.0.0-style01/shacl-shacl.ttl diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v2.0.0-style01/shacl.ttl similarity index 100% rename from testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl.ttl rename to testlib/src/main/resources/rdf/ttl/expected/v2.0.0-style01/shacl.ttl diff --git a/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/anonymous-property-lists.ttl b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/anonymous-property-lists.ttl new file mode 100644 index 0000000000..72a2e64813 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/anonymous-property-lists.ttl @@ -0,0 +1,11 @@ +@prefix xsd: . +@prefix ex: . + +ex:aThing ex:has [ + ex:value "a" ; + ] ; + ex:has [ + ex:value "z" ; + ] . + + diff --git a/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/labeled-blank-nodes.ttl b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/labeled-blank-nodes.ttl new file mode 100644 index 0000000000..72a2e64813 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/expected/labeled-blank-nodes.ttl @@ -0,0 +1,11 @@ +@prefix xsd: . +@prefix ex: . + +ex:aThing ex:has [ + ex:value "a" ; + ] ; + ex:has [ + ex:value "z" ; + ] . + + diff --git a/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/anonymous-property-lists.ttl b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/anonymous-property-lists.ttl new file mode 100644 index 0000000000..e593f9cc40 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/anonymous-property-lists.ttl @@ -0,0 +1,8 @@ +@prefix ex: . + +ex:aThing ex:has [ + ex:value "z" ; + ] ; + ex:has [ + ex:value "a" ; + ] . \ No newline at end of file diff --git a/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/labeled-blank-nodes.ttl b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/labeled-blank-nodes.ttl new file mode 100644 index 0000000000..87b43aea70 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/v2.0.1-stable-blank-node-order/input/labeled-blank-nodes.ttl @@ -0,0 +1,8 @@ +@prefix ex: . + +_:second ex:value "z" . + +ex:aThing ex:has _:first ; + ex:has _:second . + +_:first ex:value "a" . \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java b/testlib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java new file mode 100644 index 0000000000..4c8be1c136 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2024-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +class ConfigurationCacheHackListTest { + + /** Step whose equality/hashCode/serialization forces state evaluation. */ + private static FormatterStep lazyStep(String name, AtomicInteger stateEvals, Serializable state) { + return FormatterStep.createLazy(name, + () -> { + stateEvals.incrementAndGet(); + return state; + }, + SerializedFunction.identity(), + eq -> (FormatterFunc) (s -> s)); + } + + /** Step whose state evaluation always fails, like an unresolvable P2/Maven dependency. */ + private static FormatterStep explodingStep(String name, AtomicInteger stateEvals, String message) { + return FormatterStep.createLazy(name, + () -> { + stateEvals.incrementAndGet(); + throw new RuntimeException(message); + }, + SerializedFunction.identity(), + eq -> (FormatterFunc) (s -> s)); + } + + @Test + void toStringReportsSerializationFailureWithoutReEvaluating() throws Exception { + AtomicInteger evals = new AtomicInteger(); + ConfigurationCacheHackList list = ConfigurationCacheHackList.forEquality(); + list.addAll(List.of(explodingStep("unresolvable", evals, "P2 dependencies not predeclared"))); + + try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) { + assertThatThrownBy(() -> out.writeObject(list)).isNotNull(); + } + int evalsAfterSerialize = evals.get(); + assertThat(evalsAfterSerialize).as("serialization evaluates state").isPositive(); + + // Gradle renders this value into "cannot be serialized" and drops the cause, so the + // actionable message has to survive here or the user never sees it (#3004). + assertThat(list.toString()).contains("P2 dependencies not predeclared"); + assertThat(evals.get()).as("toString must not re-evaluate step state").isEqualTo(evalsAfterSerialize); + } + + @Test + void toStringDoesNotEvaluateStepState() { + AtomicInteger evals = new AtomicInteger(); + ConfigurationCacheHackList list = ConfigurationCacheHackList.forEquality(); + list.addAll(List.of(lazyStep("expensive", evals, "state"))); + + // Gradle includes this value in "cannot be serialized" error messages. + // Default Object.toString() calls hashCode(), which fingerprints steps and + // may provision P2 deps โ€” re-triggering the failure being reported (#3004). + String text = list.toString(); + assertThat(text).contains("ConfigurationCacheHackList"); + assertThat(text).contains("optimizeForEquality=true"); + assertThat(text).contains("size=1"); + assertThat(evals.get()).as("toString must not evaluate step state").isZero(); + } + + @Test + void equalityListRoundtripsThroughJavaSerialization() throws Exception { + AtomicInteger evals = new AtomicInteger(); + ConfigurationCacheHackList original = ConfigurationCacheHackList.forEquality(); + original.addAll(List.of(lazyStep("plain", evals, "eq-state"))); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { + out.writeObject(original); + } + assertThat(evals.get()).as("serializing equality list evaluates state once").isEqualTo(1); + + ConfigurationCacheHackList restored; + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + restored = (ConfigurationCacheHackList) in.readObject(); + } + assertThat(restored.getSteps()).hasSize(1); + assertThat(restored.getSteps().get(0).getName()).isEqualTo("plain"); + // toString after restore must still be side-effect free + assertThatCode(restored::toString).doesNotThrowAnyException(); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java index b41a9157f4..9583c80bb7 100644 --- a/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/asciidoc/AdocfmtStepTest.java @@ -36,6 +36,10 @@ void complexBehavior() { config.normalizeListBullets = true; config.normalizeOrderedListMarkers = true; config.ensureSourceDelimiters = true; + config.formatTables = true; + config.tableLayout = "AUTO"; + config.tableMaxLineWidth = 120; + config.tableBlankLines = "ALL"; FormatterStep step = AdocfmtStep.create(AdocfmtStep.defaultVersion(), TestProvisioner.mavenCentral(), config); StepHarness.forStep(step).testResource("asciidoc/adocfmt/dirty.adoc", "asciidoc/adocfmt/clean_complex.adoc"); } @@ -56,6 +60,8 @@ void equality() { // fields drive create(); never mutate a config object after passing it to create() boolean titleCase = false; boolean normalizeListBullets = false; + boolean formatTables = true; + String tableLayout = "AUTO"; @Override protected void setupTest(API api) { @@ -68,6 +74,12 @@ protected void setupTest(API api) { titleCase = false; normalizeListBullets = true; api.areDifferentThan(); + normalizeListBullets = false; + formatTables = false; + api.areDifferentThan(); + formatTables = true; + tableLayout = "EXPANDED"; + api.areDifferentThan(); } @Override @@ -75,6 +87,8 @@ protected FormatterStep create() { AdocfmtConfig config = new AdocfmtConfig(); config.titleCase = titleCase; config.normalizeListBullets = normalizeListBullets; + config.formatTables = formatTables; + config.tableLayout = tableLayout; return AdocfmtStep.create(AdocfmtStep.defaultVersion(), TestProvisioner.mavenCentral(), config); } }.testEquals(); diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java index 4a5f8df9c2..783ca6b94e 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -325,6 +325,7 @@ void should_setFromGit_year_when_singleCommit() throws Exception { .setMessage("add") .setAuthor("Test User", "test@example.com") .setCommitter("Test User", "test@example.com") + .setSign(false) .call(); } diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java index 723429081d..25ef9e2aab 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/ExpandWildcardImportsStepTest.java @@ -126,61 +126,29 @@ void expandWildcardImports_emptyClasspath() throws Exception { FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); // Even with empty classpath, Java standard library imports should still be expanded - String simpleCode = """ - package test; - - import java.util.*; - - public class Test { - private List items; - } - """; - - String expectedOutput = """ - package test; - - import java.util.List; - - public class Test { - private List items; - } - """; - // The step should still expand java.util.* to java.util.List - StepHarness.forStep(step).test(simpleCode, expectedOutput); + StepHarness.forStep(step).testResource("java/expandwildcardimports/JavaUtilWildcardUnformatted.test", "java/expandwildcardimports/JavaUtilWildcardFormatted.test"); } @Test void expandWildcardImports_resolvesJdkXmlTypes() throws Exception { FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - String simpleCode = """ - package test; - - import java.io.*; - import org.xml.sax.InputSource; - - public class Test { - InputSource inputSource(String value) { - return new InputSource(new StringReader(value)); - } - } - """; + StepHarness.forStep(step).testResource("java/expandwildcardimports/JdkXmlTypesUnformatted.test", "java/expandwildcardimports/JdkXmlTypesFormatted.test"); + } - String expectedOutput = """ - package test; + @Test + void expandWildcardImports_keepsTypesOnlyUsedAsQualifiers() throws Exception { + FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - import java.io.StringReader; - import org.xml.sax.InputSource; + StepHarness.forStep(step).testResource("java/expandwildcardimports/QualifierTypesUnformatted.test", "java/expandwildcardimports/QualifierTypesFormatted.test"); + } - public class Test { - InputSource inputSource(String value) { - return new InputSource(new StringReader(value)); - } - } - """; + @Test + void expandWildcardImports_keepsStaticFieldsAndEnumConstants() throws Exception { + FormatterStep step = ExpandWildcardImportsStep.create(Collections.emptySet(), TestProvisioner.mavenCentral()); - StepHarness.forStep(step).test(simpleCode, expectedOutput); + StepHarness.forStep(step).testResource("java/expandwildcardimports/StaticFieldsAndEnumConstantsUnformatted.test", "java/expandwildcardimports/StaticFieldsAndEnumConstantsFormatted.test"); } @Test diff --git a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java index eaa6b12c7e..cae19f4bca 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import static org.junit.jupiter.api.condition.JRE.JAVA_20; import static org.junit.jupiter.api.condition.JRE.JAVA_21; +import static org.junit.jupiter.api.condition.JRE.JAVA_25; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -75,6 +76,15 @@ void versionBelowOneDotTenIsNotAllowed() throws Exception { .toBe("LINE_UNDEFINED google-java-format(jvm-version) You are running Spotless on JVM 21. This requires google-java-format of at least 1.17.0 (you are using 1.9). (...)"); } + @Test + @EnabledForJreRange(min = JAVA_25) + void versionBelowMinOnJava25IsNotAllowed() throws Exception { + FormatterStep step = GoogleJavaFormatStep.create("1.28.0", "AOSP", TestProvisioner.mavenCentral()); + StepHarness.forStepNoRoundtrip(step) + .expectLintsOfResource("java/googlejavaformat/JavaCodeWithLicenseUnformatted.test") + .toBe("LINE_UNDEFINED google-java-format(jvm-version) You are running Spotless on JVM " + Jvm.version() + ". This requires google-java-format of at least 1.30.0 (you are using 1.28.0). (...)"); + } + @Test void behaviorWithAospStyle() throws Exception { FormatterStep step = GoogleJavaFormatStep.create(GoogleJavaFormatStep.defaultVersion(), "AOSP", TestProvisioner.mavenCentral()); diff --git a/testlib/src/test/java/com/diffplug/spotless/java/PalantirJavaFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/PalantirJavaFormatStepTest.java index a0111ff347..2f37e53326 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/PalantirJavaFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/PalantirJavaFormatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2025 DiffPlug + * Copyright 2022-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ void behavior() throws Exception { @Test void formatJavadoc() throws Exception { - FormatterStep step = PalantirJavaFormatStep.create("2.57.0", "PALANTIR", true, TestProvisioner.mavenCentral()); + FormatterStep step = PalantirJavaFormatStep.create(PalantirJavaFormatStep.defaultVersion(), "PALANTIR", true, TestProvisioner.mavenCentral()); StepHarness.forStep(step) .testResource("java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test", "java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test") .testResource("java/palantirjavaformat/JavaCodeWithPackageUnformatted.test", "java/palantirjavaformat/JavaCodeWithPackageFormatted.test"); diff --git a/testlib/src/test/java/com/diffplug/spotless/java/PrinceOfSpaceStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/PrinceOfSpaceStepTest.java new file mode 100644 index 0000000000..853f2e4bee --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/java/PrinceOfSpaceStepTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.java; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.SerializableEqualityTester; +import com.diffplug.spotless.TestProvisioner; + +class PrinceOfSpaceStepTest extends ResourceHarness { + + private static String format(FormatterStep step, String input) { + try (Formatter formatter = Formatter.builder() + .steps(Collections.singletonList(step)) + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .build()) { + return formatter.compute(LineEnding.toUnix(input), new File("")); + } + } + + @Test + void behavior() throws Exception { + FormatterStep step = PrinceOfSpaceStep.create(TestProvisioner.mavenCentral()); + String input = "class T { void m() { int x=1;} }"; + String formatted = format(step, input); + assertThat(formatted).contains("int x = 1;"); + assertThat(format(step, formatted)).isEqualTo(formatted); + } + + @Test + void behaviorWithOptions() throws Exception { + PrinceOfSpaceStep.Options options = new PrinceOfSpaceStep.Options(); + options.setIndentSize(2); + FormatterStep step = PrinceOfSpaceStep.create(PrinceOfSpaceStep.defaultVersion(), TestProvisioner.mavenCentral(), options); + String input = "class T { void m() { int x=1;} }"; + String formatted = format(step, input); + assertThat(formatted).contains("int x = 1;").contains("\n "); + assertThat(format(step, formatted)).isEqualTo(formatted); + } + + @Test + void equality() { + new SerializableEqualityTester() { + String version = PrinceOfSpaceStep.defaultVersion(); + Integer indentSize; + + @Override + protected void setupTest(API api) { + // same version == same + api.areDifferentThan(); + + // change the version, and it's different + version = "2.1.1"; + api.areDifferentThan(); + version = PrinceOfSpaceStep.defaultVersion(); + + // change the options, and it's different + indentSize = 4; + api.areDifferentThan(); + indentSize = null; + } + + @Override + protected FormatterStep create() { + PrinceOfSpaceStep.Options options; + if (indentSize == null) { + options = null; + } else { + options = new PrinceOfSpaceStep.Options(); + options.setIndentSize(indentSize); + } + return PrinceOfSpaceStep.create(version, TestProvisioner.mavenCentral(), options); + } + }.testEquals(); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java b/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java index 84ad3f1f61..bd63822e63 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/RemoveUnusedImportsStep_withGoogleJavaFormatTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package com.diffplug.spotless.java; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.SerializableEqualityTester; @@ -38,6 +40,20 @@ void behavior() throws Exception { ; } + /** + * {@code import module} (JEP 511) is only parseable on JDK 25+. google-java-format 1.30+ + * skips module imports in {@code RemoveUnusedImports} instead of ClassCastException on + * {@code JCModuleImport} (https://github.com/diffplug/spotless/issues/2890). + */ + @Test + @EnabledForJreRange(min = JRE.JAVA_25) + void moduleImports() throws Exception { + FormatterStep step = RemoveUnusedImportsStep.create(RemoveUnusedImportsStep.GJF, TestProvisioner.mavenCentral()); + StepHarness.forStep(step) + .testResource("java/removeunusedimports/ModuleImportUnformatted.test", "java/removeunusedimports/ModuleImportFormatted.test") + .testResource("java/removeunusedimports/ModuleImportWithUnusedUnformatted.test", "java/removeunusedimports/ModuleImportWithUnusedFormatted.test"); + } + @Test void equality() throws Exception { new SerializableEqualityTester() { diff --git a/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java new file mode 100644 index 0000000000..d4d849b99b --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/java/ShortenFullyQualifiedTypesStepTest.java @@ -0,0 +1,733 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.java; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.TestProvisioner; + +/** + * Tests for {@code shortenFullyQualifiedTypes}. + * + *

Safety contract: never introduce a compile error

+ * + *

The formatter operates without a classpath, so it cannot resolve types the way + * the compiler does. The rule is: when in doubt, leave the reference qualified. + * A verbose FQN is harmless; a wrong shortening breaks the build. + * + *

Tests are organised into three groups: + * + *

    + *
  1. Exact checks โ€” situations where we have enough information to + * decide with certainty. Examples: the simple name already appears in a + * different import, or clashes with a type declared in the same file. + * In these cases the formatter must leave the FQN alone.
  2. + * + *
  3. Expression-context heuristics โ€” FQTs used as static method call + * targets ({@code java.lang.management.ManagementFactory.getPlatformMXBeans(โ€ฆ)}), + * static field or enum-constant access, or nested-type member access. + * JavaParser sees these as {@code FieldAccessExpr}/{@code MethodCallExpr}, + * not type nodes, so two heuristics guard against false positives: + *
      + *
    • Known-package check: if the candidate's package already + * appears in the file's imports, own package, or {@code java.lang}, + * we trust it.
    • + *
    • Minimum-depth fallback: otherwise we require โ‰ฅ 2 lowercase + * (package) segments before the first uppercase (type) segment. + * This rejects {@code variable.Field} patterns that look like a FQT + * but are really field access on a local variable. + * In theory a legitimate single-segment package ({@code a.MyType}) + * with no matching import would be skipped, but single-segment + * packages are virtually non-existent in real-world Java.
    • + *
  4. + * + *
  5. Known ambiguous / leave-as-is โ€” cases where shortening could + * change semantics and the formatter cannot prove safety. For example, + * a FQN whose simple name matches a same-package type used unqualified + * elsewhere in the file.
  6. + *
+ */ +class ShortenFullyQualifiedTypesStepTest { + + private FormatterStep step() { + return ShortenFullyQualifiedTypesStep.create(TestProvisioner.mavenCentral()); + } + + private String apply(String input) throws Exception { + return step().format(LineEnding.toUnix(input), new File("")); + } + + /** Returns the code portion (everything after imports/package), for asserting FQNs are gone from code only. */ + private static String codeBody(String source) { + // Strip lines starting with package/import to avoid matching FQNs inside import statements + return source.lines() + .filter(l -> !l.stripLeading().startsWith("package ") && !l.stripLeading().startsWith("import ")) + .reduce("", (a, b) -> a + "\n" + b); + } + + @Test + void basicFqnShortening() throws Exception { + String before = String.join("\n", + "package com.example.service;", + "", + "public class UserService {", + " private final java.util.Map> cache = new java.util.HashMap<>();", + "", + " public java.util.List getUsers(java.util.function.Predicate filter) throws java.io.IOException {", + " java.util.List result = new java.util.ArrayList<>();", + " return result;", + " }", + "}", + ""); + String result = apply(before); + // Verify FQNs are shortened in code (not in imports) + assertFalse(result.contains("java.util.Map<"), "java.util.Map should be shortened"); + assertFalse(result.contains("java.util.List<"), "java.util.List should be shortened"); + assertFalse(result.contains("new java.util.HashMap"), "java.util.HashMap should be shortened"); + assertFalse(result.contains("new java.util.ArrayList"), "java.util.ArrayList should be shortened"); + assertFalse(result.contains("throws java.io.IOException"), "java.io.IOException should be shortened in throws"); + // Verify imports are added + assertTrue(result.contains("import java.util.Map;"), "should import Map"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + assertTrue(result.contains("import java.util.HashMap;"), "should import HashMap"); + assertTrue(result.contains("import java.util.ArrayList;"), "should import ArrayList"); + assertTrue(result.contains("import java.io.IOException;"), "should import IOException"); + } + + @Test + void conflictingSimpleNamesNotShortened() throws Exception { + String code = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " java.util.List a;", + " java.awt.List b;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void existingImportConflict() throws Exception { + String code = String.join("\n", + "package com.example;", + "", + "import java.awt.List;", + "", + "public class Foo {", + " java.util.List a;", + " List b;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void javaLangNotImported() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " java.lang.String s;", + "}", + ""); + String result = apply(before); + assertFalse(result.contains("java.lang.String"), "java.lang.String should be shortened"); + assertFalse(result.contains("import java.lang.String"), "java.lang.String should not be imported"); + } + + @Test + void samePackageNotImported() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " com.example.Bar b;", + "}", + ""); + String result = apply(before); + assertFalse(result.contains("com.example.Bar"), "same-package FQN should be shortened"); + assertFalse(result.contains("import com.example.Bar"), "same-package type should not be imported"); + } + + @Test + void alreadyImportedNotDuplicated() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "import java.util.List;", + "", + "public class Foo {", + " java.util.List a;", + " List b;", + "}", + ""); + String result = apply(before); + assertFalse(result.contains("java.util.List<"), "FQN should be shortened"); + int count = result.split("import java\\.util\\.List;", -1).length - 1; + assertEquals(1, count, "should not duplicate import"); + } + + @Test + void noFqnUnchanged() throws Exception { + String code = String.join("\n", + "package com.example;", + "", + "import java.util.List;", + "", + "public class Foo {", + " List a;", + "}", + ""); + assertEquals(code, apply(code)); + } + + // โ”€โ”€ Java 14+ syntax tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + @Test + void instanceofPatternMatching() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " void test(Object o) {", + " if (o instanceof java.util.List list) {", + " System.out.println(list);", + " }", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "FQN in instanceof pattern should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should add import"); + assertTrue(codeBody(result).contains("instanceof List list"), "pattern variable should be preserved"); + } + + @Test + void instanceofChainedPatterns() throws Exception { + // Two instanceof patterns with FQNs on the same line + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " void test(Object a, Object b) {", + " if (a instanceof java.util.List list", + " && b instanceof java.util.Map map) {", + " System.out.println(list);", + " }", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "FQN List should be shortened"); + assertFalse(codeBody(result).contains("java.util.Map"), "FQN Map should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + assertTrue(result.contains("import java.util.Map;"), "should import Map"); + } + + @Test + void switchPatternMatching() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " String test(Object o) {", + " return switch (o) {", + " case java.util.List list -> list.toString();", + " case java.util.Map map -> map.toString();", + " default -> \"other\";", + " };", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "FQN in switch case should be shortened"); + assertFalse(codeBody(result).contains("java.util.Map"), "FQN in switch case should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + assertTrue(result.contains("import java.util.Map;"), "should import Map"); + } + + @Test + void recordComponents() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public record Pair(java.util.List left, java.util.Map right) {}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "FQN in record component should be shortened"); + assertFalse(codeBody(result).contains("java.util.Map"), "FQN in record component should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + assertTrue(result.contains("import java.util.Map;"), "should import Map"); + } + + @Test + void sealedPermitsNotCorrupted() throws Exception { + // sealed/permits are contextual keywords โ€” ensure the step doesn't corrupt them + String code = String.join("\n", + "package com.example;", + "", + "public sealed interface Shape permits Circle, Square {}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void textBlockWithFqnUntouched() throws Exception { + String code = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " String s = \"\"\"", + " java.util.List is a type", + " \"\"\";", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void varWithFqnInGenerics() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " void test() {", + " var list = new java.util.ArrayList>();", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.ArrayList"), "FQN ArrayList should be shortened"); + assertFalse(codeBody(result).contains("java.util.Map"), "FQN Map in generic should be shortened"); + assertTrue(result.contains("import java.util.ArrayList;"), "should import ArrayList"); + assertTrue(result.contains("import java.util.Map;"), "should import Map"); + } + + @Test + void lambdaParameterTypes() throws Exception { + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " Runnable r = () -> {", + " java.util.List items = new java.util.ArrayList<>();", + " items.forEach((java.util.function.Consumer) s -> {});", + " };", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List<"), "FQN in lambda body should be shortened"); + assertFalse(codeBody(result).contains("java.util.function.Consumer"), "FQN cast in lambda should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + } + + @Test + void fqnCollisionWithEnclosingClassName() throws Exception { + // dev.jbang.cli.Alias intentionally uses dev.jbang.catalog.Alias as FQN + // because the simple name "Alias" would clash with the enclosing class + String code = String.join("\n", + "package dev.jbang.cli;", + "", + "public class Alias {", + " dev.jbang.catalog.Alias catalogAlias;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void fqnCollisionWithInnerClassName() throws Exception { + // FQN whose simple name matches an inner class declared in the same file + String code = String.join("\n", + "package com.example;", + "", + "public class Outer {", + " static class Conflict {}", + " com.other.Conflict externalConflict;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void fqnCollisionWithUnqualifiedSamePackageType() throws Exception { + // RandomAccessFile is declared in another file in this package; importing java.io.RandomAccessFile + // would silently change which type the unqualified superclass name resolves to + String code = String.join("\n", + "package test.reprod1;", + "", + "public class ClassA extends RandomAccessFile {", + " final java.io.RandomAccessFile file;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void fqnNoCollisionWithDifferentSimpleName() throws Exception { + // FQN whose simple name does NOT match the enclosing class โ€” should still shorten + String before = String.join("\n", + "package dev.jbang.cli;", + "", + "public class Alias {", + " java.util.List items;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "non-conflicting FQN should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + } + + @Test + void issue3039_fqtInStaticMethodCall() throws Exception { + String before = String.join("\n", + "import java.lang.management.BufferPoolMXBean;", + "import java.util.List;", + "", + "public class ClassA {", + " public void methodA() {", + " final List pools = java.lang.management.ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.lang.management.ManagementFactory"), + "FQT in static method call should be shortened"); + assertTrue(result.contains("import java.lang.management.ManagementFactory;"), + "should add ManagementFactory import"); + } + + @Test + void issue3039_fqtNestedTypeFieldAccess() throws Exception { + String before = String.join("\n", + "import java.util.List;", + "import pkg.models.CustomTypeProperty;", + "", + "public class ClassB {", + " final List connectionProps = List.of(new CustomTypeProperty().name(\"host\")", + " .type(pkg.models.CustomTypeProperty.TypeEnum.STRING));", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("pkg.models.CustomTypeProperty"), + "FQT in nested type field access should be shortened"); + } + + @Test + void exprStaticFieldAccess() throws Exception { + String before = String.join("\n", + "import java.util.concurrent.TimeUnit;", + "", + "public class Foo {", + " long millis = java.util.concurrent.TimeUnit.SECONDS.toMillis(5);", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.concurrent.TimeUnit"), + "FQT for static field access should be shortened"); + } + + @Test + void exprJavaLangImplicitPackage() throws Exception { + // java.lang is always known โ€” should shorten even without explicit imports + String before = String.join("\n", + "public class Foo {", + " void test() {", + " java.lang.System.exit(0);", + " }", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.lang.System"), + "java.lang.System should be shortened (java.lang is implicit)"); + assertFalse(result.contains("import java.lang.System"), + "java.lang types should not be imported"); + } + + @Test + void exprChainedAfterStaticMethod() throws Exception { + String before = String.join("\n", + "import java.util.List;", + "", + "public class Foo {", + " List items = java.util.Collections.unmodifiableList(new java.util.ArrayList<>());", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.Collections"), + "FQT in chained static method call should be shortened"); + assertTrue(result.contains("import java.util.Collections;"), "should import Collections"); + } + + @Test + void exprEnumConstantAccess() throws Exception { + String before = String.join("\n", + "import java.time.LocalDate;", + "", + "public class Foo {", + " Object day = java.time.DayOfWeek.MONDAY;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.time.DayOfWeek"), + "FQT for enum constant should be shortened"); + assertTrue(result.contains("import java.time.DayOfWeek;"), "should import DayOfWeek"); + } + + // โ”€โ”€ Expression-context: should NOT shorten (ambiguous) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + @Test + void exprSingleSegmentUnknownPackageNotShortened() throws Exception { + // 'config' could be a local variable; only 1 lowercase segment, no matching import + String code = String.join("\n", + "public class Foo {", + " Object v = config.Default.VALUE;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void exprSingleSegmentUnknownPackageMethodNotShortened() throws Exception { + // 'builder' could be a local variable; only 1 lowercase segment, no matching import + String code = String.join("\n", + "public class Foo {", + " Object v = builder.Type.create();", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void exprSingleSegmentWithKnownImportDoesShorten() throws Exception { + // 'config' has 1 lowercase segment but IS a known package (import exists from config.*) + String before = String.join("\n", + "import config.Other;", + "", + "public class Foo {", + " Object v = config.Default.VALUE;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("config.Default"), + "known single-segment package should be shortened"); + } + + @Test + void exprCollisionWithExistingImportNotShortened() throws Exception { + // java.util.List is already imported; java.awt.List in expression context must not shorten + String code = String.join("\n", + "import java.util.List;", + "", + "public class Foo {", + " int n = java.awt.List.COLUMN_HEADERS;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void exprCollisionWithDeclaredTypeNotShortened() throws Exception { + // File declares class named 'Entry'; expression FQT with same simple name must not shorten + String code = String.join("\n", + "import java.util.Map;", + "", + "public class Entry {", + " Object e = java.util.Map.Entry.class;", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void inheritedMemberTypeFromAnotherFileIsNotShortened() throws Exception { + // Base lives in another file, so a member type named Type is invisible here. + // Shortening external.Type would resolve to that member. #3117 + String code = String.join("\n", + "package repro;", + "", + "public class Child extends Base {", + " public static String use(external.Type value) {", + " return value.externalOnly();", + " }", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void nestedClassInsideUnknownSupertypeIsNotShortened() throws Exception { + String code = String.join("\n", + "package repro;", + "", + "public class Child extends Base {", + " static class Inner {", + " external.Type value;", + " }", + "}", + ""); + assertEquals(code, apply(code)); + } + + @Test + void sameFqnShortensOnlyOutsideUnknownInheritance() throws Exception { + String before = String.join("\n", + "package repro;", + "", + "class Child extends Base {", + " external.Type value;", + "}", + "", + "class Other {", + " external.Type value;", + "}", + ""); + String result = apply(before); + assertTrue(result.contains("class Child extends Base {\n external.Type value;"), result); + assertFalse(result.contains("class Other {\n external.Type value;"), result); + assertTrue(result.contains("import external.Type;"), result); + } + + @Test + void inheritedMemberTypeInSameFileBlocksOnlyThatName() throws Exception { + String before = String.join("\n", + "package repro;", + "", + "class Base {", + " static class Type {}", + "}", + "", + "public class Child extends Base {", + " external.Type value;", + " java.util.List items;", + "}", + ""); + String result = apply(before); + assertTrue(codeBody(result).contains("external.Type"), "inherited member name must stay qualified: " + result); + assertFalse(codeBody(result).contains("java.util.List"), "unrelated FQN should still be shortened: " + result); + assertTrue(result.contains("import java.util.List;"), result); + assertFalse(result.contains("import external.Type;"), result); + } + + @Test + void extendsInFileTypeWithNoMemberTypesStillShortens() throws Exception { + String before = String.join("\n", + "package repro;", + "", + "class Base {}", + "", + "public class Child extends Base {", + " java.util.List items;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), result); + assertTrue(result.contains("import java.util.List;"), result); + } + + @Test + void multipleAnnotationsWithFqn() throws Exception { + // FQNs used as annotation types should NOT be treated as type references + // (annotations start with @, not handled by ClassOrInterfaceType) + // but FQN types in annotation values or alongside annotations should work + String before = String.join("\n", + "package com.example;", + "", + "public class Foo {", + " java.util.List items;", + "}", + ""); + String result = apply(before); + assertFalse(codeBody(result).contains("java.util.List"), "FQN should be shortened"); + assertTrue(result.contains("import java.util.List;"), "should import List"); + } + + @Test + void issue3133_methodReferenceOnFieldNotShortened() throws Exception { + // `localVar.field::method` is a field access on an expression, parsed by JavaParser + // as a ClassOrInterfaceType scope. It must NOT be treated as a fully-qualified type. + String before = String.join("\n", + "package com.example;", + "", + "import java.util.Map;", + "import java.util.function.Function;", + "", + "public class Example {", + " static class Catalog {", + " Map aliases;", + " }", + "", + " String demo(Catalog catalog) {", + " return find(catalog.aliases::get);", + " }", + "", + " String find(Function f) {", + " return f.apply(\"x\");", + " }", + "}", + ""); + String result = apply(before); + assertTrue(result.contains("catalog.aliases::get"), "field method reference must be left untouched"); + assertFalse(result.contains("import catalog."), "must not add a bogus import: " + result); + } + + @Test + void methodReferenceOnRealTypeStillShortened() throws Exception { + // A genuine fully-qualified type method reference should still be shortened. + String before = String.join("\n", + "package com.example;", + "", + "import java.util.function.Function;", + "", + "public class Example {", + " Function f() {", + " return java.lang.Integer::parseInt;", + " }", + "}", + ""); + String result = apply(before); + assertFalse(result.contains("java.lang.Integer::parseInt"), "FQN type method reference should be shortened"); + assertTrue(result.contains("Integer::parseInt"), "should shorten to Integer::parseInt"); + } + + @Test + void methodReferenceWithSingleSegmentScopeLeftAlone() throws Exception { + // `config.Database::connect` has an upper-case simple name but a single-segment, + // non-package scope (e.g. a field access). Not a known package and < 2 package + // segments, so it must be left alone rather than shortened to a bogus import. + String before = String.join("\n", + "package com.example;", + "", + "public class Example {", + " Runnable r() {", + " return config.Database::connect;", + " }", + "}", + ""); + String result = apply(before); + assertTrue(result.contains("config.Database::connect"), "single-segment non-package scope must be left alone"); + assertFalse(result.contains("import config."), "must not add a bogus import: " + result); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java index 0299c1ad3b..4e15af85cf 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java @@ -25,7 +25,7 @@ class TableTestFormatterStepTest extends ResourceHarness { - private static final String VERSION = "1.1.1"; + private static final String VERSION = TableTestFormatterStep.defaultVersion(); @Test void behavior() { diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java index 82180045c6..6efab924ee 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 DiffPlug + * Copyright 2021-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,8 +42,8 @@ void behaviorConf() throws Exception { File conf = setFile(configPath).toResource("kotlin/diktat/diktat-analysis.yml"); FileSignature config = signAsList(conf); - FormatterStep step = DiktatStep.create("1.2.1", TestProvisioner.mavenCentral(), config); - StepHarnessWithFile.forStep(this, step).expectLintsOfResource("kotlin/diktat/Unsolvable.kt").toBe("L1 diktat(diktat-ruleset:debug-print) [DEBUG_PRINT] use a dedicated logging library: found println()", + FormatterStep step = DiktatStep.create("2.0.0", TestProvisioner.mavenCentral(), config); + StepHarnessWithFile.forStep(this, step).expectLintsOfResource("kotlin/diktat/Unsolvable.kt").toBe("L12 diktat(diktat-ruleset:debug-print) [DEBUG_PRINT] use a dedicated logging library: found println()", "L13 diktat(diktat-ruleset:debug-print) [DEBUG_PRINT] use a dedicated logging library: found println()"); } diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java index 783b5d50fe..1492624a9b 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ResourceHarness; @@ -62,6 +64,20 @@ void behaviorWithOptions_0_62() { StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); } + @Test + void behavior_0_64() throws Exception { + FormatterStep step = KtfmtStep.create("0.64", TestProvisioner.mavenCentral()); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + + @Test + void behaviorWithOptions_0_64() { + KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.64", TestProvisioner.mavenCentral(), KtfmtStep.Style.GOOGLE, options); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + @Test void behaviorWithOptions_0_53() { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); @@ -79,6 +95,7 @@ void behaviorWithOptions_0_56() { } @Test + @EnabledForJreRange(max = JRE.JAVA_24) void dropboxStyle_0_16() throws Exception { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); FormatterStep step = KtfmtStep.create("0.16", TestProvisioner.mavenCentral(), KtfmtStep.Style.DROPBOX, options); @@ -86,6 +103,7 @@ void dropboxStyle_0_16() throws Exception { } @Test + @EnabledForJreRange(max = JRE.JAVA_24) void dropboxStyle_0_18() throws Exception { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); FormatterStep step = KtfmtStep.create("0.18", TestProvisioner.mavenCentral(), KtfmtStep.Style.DROPBOX, options); @@ -93,6 +111,7 @@ void dropboxStyle_0_18() throws Exception { } @Test + @EnabledForJreRange(max = JRE.JAVA_24) void dropboxStyle_0_22() throws Exception { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); FormatterStep step = KtfmtStep.create("0.22", TestProvisioner.mavenCentral(), KtfmtStep.Style.DROPBOX, options); diff --git a/testlib/src/test/java/com/diffplug/spotless/rdf/RdfFormatterTest.java b/testlib/src/test/java/com/diffplug/spotless/rdf/RdfFormatterTest.java index 95ca530caa..ff5adc1b99 100644 --- a/testlib/src/test/java/com/diffplug/spotless/rdf/RdfFormatterTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/rdf/RdfFormatterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2025 DiffPlug + * Copyright 2024-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package com.diffplug.spotless.rdf; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; @@ -24,7 +27,10 @@ import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.provider.Arguments; import org.opentest4j.AssertionFailedError; @@ -33,6 +39,7 @@ import com.diffplug.spotless.StepHarness; import com.diffplug.spotless.TestProvisioner; +@EnabledOnJre(JRE.JAVA_25) public class RdfFormatterTest extends ResourceHarness { private static FormatterStep forTurtleFormatterVersion(String version) throws ClassNotFoundException { @@ -49,17 +56,58 @@ private static FormatterStep forTurtleFormatterVersionAndStyle(String version, M public RdfFormatterTest() {} @Test - void testTurtleFormatter_1_2_12_DefaultStyle() throws IOException, ClassNotFoundException { + void testCoolRdfFormatter_2_0_0_DefaultStyle() throws IOException, ClassNotFoundException { String inputDir = "/rdf/ttl/input/"; - String expectedOutputDir = "/rdf/ttl/expected/v1.2.12-default/"; - testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersion("1.2.12"))); + String expectedOutputDir = "/rdf/ttl/expected/v2.0.0-default/"; + testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersion("2.0.0"))); } @Test - void testTurtleFormatter_1_2_12_style01() throws IOException, ClassNotFoundException { + void testCoolRdfFormatter_2_0_0_style01() throws IOException, ClassNotFoundException { String inputDir = "/rdf/ttl/input/"; - String expectedOutputDir = "/rdf/ttl/expected/v1.2.12-style01/"; - testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("1.2.12", style01()))); + String expectedOutputDir = "/rdf/ttl/expected/v2.0.0-style01/"; + testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("2.0.0", style01()))); + } + + @RepeatedTest(3) + void blankNodeOrderingIsStableInCoolRdfFormatter_2_0_1() throws IOException, ClassNotFoundException { + String inputDir = "/rdf/ttl/v2.0.1-stable-blank-node-order/input/"; + String expectedOutputDir = "/rdf/ttl/v2.0.1-stable-blank-node-order/expected/"; + Map style = Map.of("preserveBlankNodeLabelsAndOrdering", "false"); + testBeforeAfterFolders(inputDir, expectedOutputDir, StepHarness.forStep(forTurtleFormatterVersionAndStyle("2.0.1", style))); + } + + @Test + void blankNodeOrderingIsNotStableInCoolRdfFormatter_2_0_0() throws Exception { + FormatterStep step = forTurtleFormatterVersion("2.0.0"); + File file = new File("blank-node-order.ttl"); + + String alphaThenBeta = """ + @prefix ex: . + + ex:root + ex:child [ + ex:id "alpha" ; + ex:value "1" + ], [ + ex:id "beta" ; + ex:value "2" + ] . + """; + String betaThenAlpha = """ + @prefix ex: . + + ex:root + ex:child [ + ex:id "beta" ; + ex:value "2" + ], [ + ex:id "alpha" ; + ex:value "1" + ] . + """; + + assertThat(step.format(alphaThenBeta, file)).isNotEqualTo(step.format(betaThenAlpha, file)); } private static @NotNull Map defaultStyle() { diff --git a/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java index e04e46db7e..bb482dcf6f 100644 --- a/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/scala/ScalaFmtStepTest.java @@ -36,13 +36,13 @@ void behaviorDefaultConfig() { @Test void behaviorCustomConfig() { - StepHarness.forStep(ScalaFmtStep.create(ScalaFmtStep.DEFAULT_VERSION, TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf"))) + StepHarness.forStep(ScalaFmtStep.create("3.8.1", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.conf"))) .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.cleanWithCustomConf_3.0.0"); } @Test void behaviorFileOverride() { - FormatterStep step = ScalaFmtStep.create(ScalaFmtStep.DEFAULT_VERSION, TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.fileoverride.conf")); + FormatterStep step = ScalaFmtStep.create("3.8.1", TestProvisioner.mavenCentral(), createTestFile("scala/scalafmt/scalafmt.fileoverride.conf")); StepHarness.forStep(step) .testResource("scala/scalafmt/basic.dirty", "scala/scalafmt/basic.cleanWithCustomConf_3.0.0"); } diff --git a/testlib/src/test/java/com/diffplug/spotless/toml/VersionCatalogStepTest.java b/testlib/src/test/java/com/diffplug/spotless/toml/VersionCatalogStepTest.java index 23730241c6..13570ea66d 100644 --- a/testlib/src/test/java/com/diffplug/spotless/toml/VersionCatalogStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/toml/VersionCatalogStepTest.java @@ -15,9 +15,18 @@ */ package com.diffplug.spotless.toml; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; + import org.junit.jupiter.api.Test; +import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintState; import com.diffplug.spotless.SerializableEqualityTester; import com.diffplug.spotless.StepHarness; @@ -82,6 +91,19 @@ void commentsPreserved() throws Exception { "[versions]\n# A library\nalpha = \"2.0\"\n# Z library\nzoo = \"1.0\"\n"); } + @Test + void standaloneCommentsAtSectionBoundariesAndEofArePreserved() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).testUnaffected( + "[versions]\nzoo = \"1.0\"\n# keep this trailing comment\n\n[libraries]\nfoo = { module = \"g:a\", version.ref = \"zoo\" }\n# keep this final comment\n"); + } + + @Test + void trailingCommentsFollowTheirSectionWhenTablesAreSorted() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[plugins]\nzoo = \"g:z:1\"\n# plugin note\n[versions]\nalpha = \"1.0\"\n# version note\n", + "[versions]\nalpha = \"1.0\"\n# version note\n\n[plugins]\nzoo = \"g:z:1\"\n# plugin note\n"); + } + @Test void inlineCommentsPreserved() throws Exception { StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); @@ -90,6 +112,146 @@ void inlineCommentsPreserved() throws Exception { "[versions]\nfoo = \"1.0\" # latest stable\n"); } + @Test + void bracketsInInlineCommentsDoNotConsumeFollowingEntries() throws Exception { + StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); + for (String comment : new String[]{"[planned update", "]", "{", "}", "[\"unfinished quote"}) { + harness.test( + "[versions]\nzoo = \"1.0\" # " + comment + "\nalpha = \"2.0\"\n", + "[versions]\nalpha = \"2.0\"\nzoo = \"1.0\" # " + comment + "\n"); + } + } + + @Test + void hashesAndBracketsInsideStringsAreNotComments() throws Exception { + StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); + for (String value : new String[]{"\"1.0#[\"", "'1.0#['", "\"1.0\\\"#[\""}) { + harness.testUnaffected("[versions]\nfoo = " + value + "\n"); + } + } + + @Test + void escapedBackslashBeforeClosingQuoteDoesNotHideComment() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[versions]\nzoo = \"1.0\\\\\" # [planned update\nalpha = \"2.0\"\n", + "[versions]\nalpha = \"2.0\"\nzoo = \"1.0\\\\\" # [planned update\n"); + } + + @Test + void literalStringCommasInArraysArePreserved() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[bundles]\nfoo=['a,b','c']\n", + "[bundles]\nfoo = [ 'a,b', 'c' ]\n"); + } + + @Test + void literalStringCommasInInlineTablesArePreserved() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[libraries]\nfoo={module='a,b',version=\"1.0\"}\n", + "[libraries]\nfoo = { module = 'a,b', version = \"1.0\" }\n"); + } + + @Test + void escapedBackslashBeforeArrayDelimiterDoesNotChangeStrings() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[bundles]\nfoo=[\"a\\\\\",\"b,c\"]\n", + "[bundles]\nfoo = [ \"a\\\\\", \"b,c\" ]\n"); + } + + @Test + void escapedBackslashBeforeTableDelimiterDoesNotChangeStrings() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[libraries]\nfoo={module=\"a\\\\\",version=\"b,c\"}\n", + "[libraries]\nfoo = { module = \"a\\\\\", version = \"b,c\" }\n"); + } + + @Test + void singleLineStringsCannotConsumeFollowingEntries() { + for (String quote : new String[]{"\"", "'"}) { + StepHarness.forStep(VersionCatalogStep.create()) + .expectLintsOf("[versions]\nfoo = " + quote + "1.0\nbar = " + quote + "2.0\n") + .toBe("L2 versionCatalog(unterminatedEntry) Unterminated version catalog entry in [versions]"); + } + } + + @Test + void escapedNewlineCannotContinueSingleLineString() { + StepHarness.forStep(VersionCatalogStep.create()) + .expectLintsOf("[versions]\nfoo = \"1.0\\\nbar = \"2.0\n") + .toBe("L2 versionCatalog(unterminatedEntry) Unterminated version catalog entry in [versions]"); + } + + @Test + void tripleQuotedStringCommasInArraysArePreserved() throws Exception { + StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); + for (String delimiter : new String[]{"\"\"\"", "'''"}) { + harness.test( + "[bundles]\nfoo=[" + delimiter + "a,b" + delimiter + ",'c']\n", + "[bundles]\nfoo = [ " + delimiter + "a,b" + delimiter + ", 'c' ]\n"); + } + } + + @Test + void multilineArrayCommentsKeepTheirLineBoundaries() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[bundles]\nzoo = [\"c\"]\n\"alpha\" = [\n \"a\", # [keep this note\n # } another note\n \"b\"\n]\n", + "[bundles]\n\"alpha\" = [\n \"a\", # [keep this note\n # } another note\n \"b\"\n]\nzoo = [ \"c\" ]\n"); + } + + @Test + void multilineArrayWithoutCommentsStillJoins() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).test( + "[bundles]\nfoo = [\n \"a\",\n \"b\"\n]\n", + "[bundles]\nfoo = [ \"a\", \"b\" ]\n"); + } + + @Test + void preservedMultilineEntriesStillStripQuotedKeys() throws Exception { + StepHarness.forStep(VersionCatalogStep.create(true)).test( + "[bundles]\n\"foo\" = [\n \"a#b\" # keep this note\n]\n", + "[bundles]\nfoo = [\n \"a#b\" # keep this note\n]\n"); + } + + @Test + void quotedCommentAndMultilineMarkersDoNotPreventJoiningArrays() throws Exception { + StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); + for (String value : new String[]{"\"a#b\"", "'a#b'", "\"'''\"", "'\"\"\"'"}) { + harness.test( + "[bundles]\nfoo = [\n " + value + "\n]\n", + "[bundles]\nfoo = [ " + value + " ]\n"); + } + } + + @Test + void multilineBasicStringCanContinueAfterEscapedNewline() throws Exception { + StepHarness.forStep(VersionCatalogStep.create()).testUnaffected( + "[versions]\nfoo = \"\"\"1.0\\\n continued\"\"\"\n"); + } + + @Test + void multilineStringsKeepHashesBracketsAndWhitespace() throws Exception { + StepHarness harness = StepHarness.forStep(VersionCatalogStep.create()); + for (String delimiter : new String[]{"\"\"\"", "'''"}) { + harness.testUnaffected("[versions]\nfoo = " + delimiter + "1.0 \n# [literal text\n" + delimiter + "\n"); + } + } + + @Test + void unfinishedEntryDoesNotReturnPartialCatalog() throws Exception { + String input = "# Catalog\n\n[bundles]\nalpha=[\"a\"]\nzoo = [\n \"b\"\n"; + try (Formatter formatter = Formatter.builder() + .steps(List.of(VersionCatalogStep.create())) + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .build()) { + StepHarness.forFormatter(formatter).expectLintsOf(input) + .toBe("L5 versionCatalog(unterminatedEntry) Unterminated version catalog entry in [bundles]"); + LintState state = LintState.of(formatter, new File("libs.versions.toml"), input.getBytes(StandardCharsets.UTF_8)); + assertThat(state.isHasLints()).isTrue(); + assertThat(state.getDirtyState().isClean()).isTrue(); + } + } + @Test void inlineCommentOnInlineTable() throws Exception { StepHarness harness = StepHarness.forStep(VersionCatalogStep.create());