diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8afdc61345..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,385 +0,0 @@ -version: 2.1 -orbs: - win: circleci/windows@2.4.0 - -anchors: - env_gradle: &env_gradle - environment: - # we're only allowed to use 2 vCPUs - GRADLE_OPTS: "-Dorg.gradle.workers.max=2" - docker: - - image: cimg/openjdk:11.0 - env_gradle_large: &env_gradle_large - << : *env_gradle - resource_class: large # https://circleci.com/docs/2.0/configuration-reference/#resource_class - environment: - GRADLE_OPTS: "-Dorg.gradle.workers.max=4" - - restore_cache_wrapper: &restore_cache_wrapper - restore_cache: - key: gradle-wrapper2-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - restore_cache_deps: &restore_cache_deps - restore_cache: - keys: - - gradle-deps3-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }} - - gradle-deps3- - set_git_origin_to_https: &set_git_origin_to_https - run: - name: set git origin to https - command: git remote set-url --push origin https://github.com/diffplug/spotless - - test_nomaven: &test_nomaven - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - run: - name: gradlew check -x spotlessCheck - command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew check -x spotlessCheck --build-cache - - store_test_results: - path: testlib/build/test-results/test - - store_test_results: - path: lib-extra/build/test-results/test - - store_test_results: - path: plugin-gradle/build/test-results/test - - store_artifacts: - path: lib/build/spotbugs - - store_artifacts: - path: lib-extra/build/spotbugs - - store_artifacts: - path: testlib/build/spotbugs - - store_artifacts: - path: plugin-gradle/build/spotbugs - -jobs: - # gradlew spotlessCheck assemble testClasses - assemble_testClasses: - <<: *env_gradle_large - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - run: - name: gradlew spotlessCheck assemble testClasses - command: ./gradlew spotlessCheck assemble testClasses --build-cache - - save_cache: - paths: - - ~/.gradle/wrapper - key: gradle-wrapper2-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - - save_cache: - paths: - - ~/.gradle/caches - - ~/.m2 - key: gradle-deps3-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }} - test_nomaven_11: - # latest LTS version - <<: *env_gradle_large - docker: - - image: cimg/openjdk:11.0 - <<: *test_nomaven - test_nomaven_17: - # latest JDK - <<: *env_gradle_large - docker: - - image: cimg/openjdk:17.0 - <<: *test_nomaven - test_justmaven_11: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - run: - name: gradlew :plugin-maven:check - command: ./gradlew :plugin-maven:check --build-cache - - store_test_results: - path: plugin-maven/build/test-results/test - test_npm_8: - << : *env_gradle - environment: - # java doesn't play nice with containers, it tries to hog the entire machine - # https://circleci.com/blog/how-to-handle-java-oom-errors/ - # try the experimental JVM option - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" - docker: - - image: cimg/openjdk:8.0-node - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - run: - name: gradlew testNpm - command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew testNpm --build-cache - - store_test_results: - path: testlib/build/test-results/testNpm - - store_test_results: - path: plugin-maven/build/test-results/testNpm - - store_test_results: - path: plugin-gradle/build/test-results/testNpm - - run: - name: gradlew test - command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew test --build-cache - - store_test_results: - path: testlib/build/test-results/test - - store_test_results: - path: lib-extra/build/test-results/test - - store_test_results: - path: plugin-gradle/build/test-results/test - test_windows: - executor: - name: win/default - shell: cmd.exe - steps: - - checkout - - run: - name: gradlew test - command: gradlew test --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true - - store_test_results: - path: testlib/build/test-results/test - - store_test_results: - path: lib-extra/build/test-results/test - - store_test_results: - path: plugin-gradle/build/test-results/test - - run: - name: gradlew testNpm - command: gradlew testNpm --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true - - store_test_results: - path: testlib/build/test-results/testNpm - - store_test_results: - path: plugin-gradle/build/test-results/testNpm - changelog_print: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - run: - name: gradlew changelogPrint - command: ./gradlew changelogPrint - do_release_all: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :changelogPush - command: ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all - - run: - name: gradlew :plugin-gradle:changelogPush - command: ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${gradle_key} -Pgradle.publish.secret=${gradle_secret} --stacktrace --warning-mode all - - run: - name: gradlew :plugin-maven:changelogPush - command: ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace --warning-mode all - do_release_lib: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :changelogPush - command: ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all - do_release_plugin_gradle: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :plugin-gradle:changelogPush - command: ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${gradle_key} -Pgradle.publish.secret=${gradle_secret} --stacktrace - do_release_plugin_maven: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :plugin-maven:changelogPush - command: ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace - ext_changelog_print: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew -Pcom.diffplug.spotless.include.ext.nop2=true changelogPrint - command: ./gradlew -Pcom.diffplug.spotless.include.ext.nop2=true changelogPrint - ext_do_release_base: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :eclipse-base:changelogPush - command: ./gradlew -Pcom.diffplug.spotless.include.ext.nop2=true :eclipse-base:changelogPush -Prelease=true --stacktrace - ext_do_release_jdt: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :eclipse-jdt:changelogPush - command: ./gradlew -Pcom.diffplug.spotless.include.ext.nop2=true :eclipse-jdt:changelogPush -Prelease=true --stacktrace - ext_do_release_cdt: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :eclipse-cdt:changelogPush - command: ./gradlew -Pcom.diffplug.spotless.include.ext.cdt=true :eclipse-cdt:changelogPush -Prelease=true --stacktrace - ext_do_release_groovy: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :eclipse-groovy:changelogPush - command: ./gradlew -Pcom.diffplug.spotless.include.ext.groovy=true :eclipse-groovy:changelogPush -Prelease=true --stacktrace - ext_do_release_wtp: - << : *env_gradle - steps: - - checkout - - *restore_cache_wrapper - - *restore_cache_deps - - *set_git_origin_to_https - - run: - name: gradlew :eclipse-wtp:changelogPush - command: ./gradlew -Pcom.diffplug.spotless.include.ext.wtp=true :eclipse-wtp:changelogPush -Prelease=true --stacktrace - -workflows: - version: 2 - assemble_and_test: - jobs: - - test_windows - - assemble_testClasses - - test_justmaven_11: - requires: - - assemble_testClasses - - test_nomaven_11: - requires: - - assemble_testClasses - - test_nomaven_17: - requires: - - assemble_testClasses - - test_npm_8: - requires: - - assemble_testClasses - deploy: - jobs: - - changelog_print: - filters: - branches: - only: main - - release_all: - type: approval - requires: - - changelog_print - - do_release_all: - requires: - - release_all - context: - - SonatypeDeploy - - release_plugin_gradle: - type: approval - requires: - - changelog_print - - do_release_plugin_gradle: - requires: - - release_plugin_gradle - context: - - SonatypeDeploy - - release_plugin_maven: - type: approval - requires: - - changelog_print - - do_release_plugin_maven: - requires: - - release_plugin_maven - context: - - SonatypeDeploy - - release_lib: - type: approval - requires: - - changelog_print - - do_release_lib: - requires: - - release_lib - context: - - SonatypeDeploy - ext_deploy: - jobs: - - ext_changelog_print: - filters: - branches: - only: main - - ext_release_base: - type: approval - requires: - - ext_changelog_print - - ext_do_release_base: - requires: - - ext_release_base - context: - - SonatypeDeploy - - ext_release_jdt: - type: approval - requires: - - ext_changelog_print - - ext_do_release_jdt: - requires: - - ext_release_jdt - context: - - SonatypeDeploy - - ext_release_cdt: - type: approval - requires: - - ext_changelog_print - - ext_do_release_cdt: - filters: - branches: - only: main - requires: - - ext_release_cdt - context: - - SonatypeDeploy - - ext_release_groovy: - type: approval - requires: - - ext_changelog_print - - ext_do_release_groovy: - filters: - branches: - only: main - requires: - - ext_release_groovy - context: - - SonatypeDeploy - - ext_release_wtp: - type: approval - requires: - - ext_changelog_print - - ext_do_release_wtp: - filters: - branches: - only: main - requires: - - ext_release_wtp - context: - - SonatypeDeploy diff --git a/.editorconfig b/.editorconfig index a8061a0816..9fe2fbc6c6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,11 +11,28 @@ charset = utf-8 [*.md] indent_style = space indent_size = 2 +trim_trailing_whitespace = false [*.java] # 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_imports_layout = $*,|,java.**,|,javax.**,|,org.**,|,com.**,|,com.diffplug.**,|,* ij_java_use_single_class_imports = true +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 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# Prevent unexpected automatic indentation when crafting test-cases +[/testlib/src/main/resources/**] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +indent_size = unset +ij_formatter_enabled = false diff --git a/.gitattributes b/.gitattributes index e6141d8894..dc4cb6b0bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ * text eol=lf +*.bat eol=crlf *.png binary *.jar binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 10ef831183..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gradle" - directory: "/" - schedule: - interval: "weekly" - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..ef44cb00f9 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,15 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ], + "packageRules": [ + { + "groupName": "Ktlint", + "enabled": false, + "matchPackageNames": [ + "/com.pinterest.ktlint:*/", + ] + } + ] +} diff --git a/.github/workflows/changelog-print.yml b/.github/workflows/changelog-print.yml new file mode 100644 index 0000000000..88be1f9332 --- /dev/null +++ b/.github/workflows/changelog-print.yml @@ -0,0 +1,20 @@ +name: changelogPrint + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + name: changelogPrint + steps: + - uses: actions/checkout@v4 + - name: jdk 11 + uses: actions/setup-java@v4 + with: + java-version: 11 + distribution: 'temurin' + - name: gradle caching + uses: gradle/actions/setup-gradle@v4 + - run: ./gradlew changelogPrint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..e5de52b4d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +# BUILDCACHE_USER +# BUILDCACHE_PASS +# - rw access to buildcache.diffplug.com + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + sanityCheck: + name: spotlessCheck assemble testClasses + runs-on: ubuntu-latest + env: + buildcacheuser: ${{ secrets.BUILDCACHE_USER }} + buildcachepass: ${{ secrets.BUILDCACHE_PASS }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install JDK 11 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: 11 + - name: gradle caching + uses: gradle/actions/setup-gradle@v4 + - name: spotlessCheck + run: ./gradlew spotlessCheck + - name: assemble testClasses + run: ./gradlew assemble testClasses + build: + needs: sanityCheck + strategy: + fail-fast: false + matrix: + kind: [maven, gradle] + # Test on the latest Java version once Gradle & Maven support it. + jre: [11, 17, 21, 23] + os: [ubuntu-latest] + include: + # test windows at the diagonals of the above matrix + - kind: maven + jre: 11 + os: windows-latest + - kind: gradle + jre: 17 + os: windows-latest + # npm on linux only (crazy slow on windows) + - kind: npm + jre: 11 + os: ubuntu-latest + - kind: shfmt + jre: 11 + os: ubuntu-latest + shfmt-version: v3.8.0 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install JDK ${{ matrix.distribution }} ${{ matrix.java_version }} + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ matrix.jre }} + - name: gradle caching + uses: gradle/actions/setup-gradle@v4 + - 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@v5 + with: + go-version: 'stable' + - name: Install shfmt + if: matrix.kind == 'shfmt' + run: | + go install mvdan.cc/sh/v3/cmd/shfmt@${{ matrix.shfmt-version }} + - name: Test shfmt + if: matrix.kind == 'shfmt' + run: ./gradlew testShfmt + - name: junit result + uses: mikepenz/action-junit-report@v5 + if: always() # always run even if the previous step fails + with: + check_name: JUnit ${{ matrix.kind }} ${{ matrix.jre }} ${{ matrix.os }} + report_paths: '*/build/test-results/*/TEST-*.xml' + check_retries: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8af7ad4b44..5e41395eb0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,8 +14,15 @@ on: schedule: - cron: '0 16 * * 3' +permissions: + contents: read + jobs: analyze: + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report name: Analyze runs-on: ubuntu-latest @@ -30,7 +37,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -43,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +61,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +75,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..ffc164a461 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,67 @@ +# 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); +# GPG_PASSPHRASE +# GPG_KEY64 (base64) +# gpg --export-secret-keys --armor KEY_ID | openssl base64 | pbcopy +# GRADLE_KEY +# GRADLE_SECRET + +name: deploy +on: + workflow_dispatch: + inputs: + to_publish: + description: 'What to publish' + required: true + default: 'all' + type: choice + options: + - plugin-gradle + - plugin-maven + - all + - lib + +jobs: + build: + runs-on: ubuntu-latest + name: deploy + 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 }} + steps: + - uses: actions/checkout@v4 + - name: jdk 11 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'temurin' + - name: gradle caching + uses: gradle/actions/setup-gradle@v4 + - name: git fetch origin main + run: git fetch origin main + - name: publish all + if: "${{ github.event.inputs.to_publish == 'all' }}" + 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 + - name: publish just plugin-gradle + if: "${{ github.event.inputs.to_publish == 'plugin-gradle' }}" + 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 + - 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 + - name: publish just lib + if: "${{ github.event.inputs.to_publish == 'lib' }}" + run: | + ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index 405a2b3065..0000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 diff --git a/.gitignore b/.gitignore index 8b95e1b44f..ab04bc7021 100644 --- a/.gitignore +++ b/.gitignore @@ -19,11 +19,7 @@ gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar -#Gradle-Lock files in _ext just serving as an input for lib-extra -_ext/*/gradle/dependency-locks/*.lockfile - ### Eclipse ### -*.pydevproject .metadata .gradle bin/ @@ -122,3 +118,9 @@ nbdist/ nbactions.xml nb-configuration.xml .nb-gradle/ + +# MacOS jenv +.java-version + +# VS Code +.vscode/ diff --git a/CHANGES.md b/CHANGES.md index 0a9b6ff2fc..0a043ffa24 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,11 +10,458 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] + +## [3.1.1] - 2025-04-07 +### Changed +* Use palantir-java-format 2.57.0 on Java 21. ([#2447](https://github.com/diffplug/spotless/pull/2447)) +* Re-try `npm install` with `--prefer-online` after `ERESOLVE` error. ([#2448](https://github.com/diffplug/spotless/pull/2448)) + +## [3.1.0] - 2025-02-20 +### Added +* Support for`clang-format` on maven-plugin ([#2406](https://github.com/diffplug/spotless/pull/2406)) +* Allow overriding classLoader for all `JarState`s to enable spotless-cli ([#2427](https://github.com/diffplug/spotless/pull/2427)) + +## [3.0.2] - 2025-01-14 +### Fixed +* Node.JS-based tasks now work with the configuration cache ([#2372](https://github.com/diffplug/spotless/issues/2372)) +* Eclipse-based tasks can now handle parallel configuration ([#2389](https://github.com/diffplug/spotless/issues/2389)) + +## [3.0.1] - 2025-01-07 +### Fixed +* Deployment was missing part of the CDT formatter, now fixed. ([#2384](https://github.com/diffplug/spotless/issues/2384)) + +## [3.0.0] - 2025-01-06 +## Headline changes +* All steps now support roundtrip serialization (end of [#987](https://github.com/diffplug/spotless/issues/987)). +* Spotless now supports [linting](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#lints) in addition to formatting. +### Changed +* Allow setting Eclipse config from a string, not only from files ([#2337](https://github.com/diffplug/spotless/pull/2337)) +* Bump default `ktlint` version to latest `1.3.0` -> `1.4.0`. ([#2314](https://github.com/diffplug/spotless/pull/2314)) +* Add _Sort Members_ feature based on [Eclipse JDT](plugin-gradle/README.md#eclipse-jdt) implementation. ([#2312](https://github.com/diffplug/spotless/pull/2312)) +* Bump default `jackson` version to latest `2.18.0` -> `2.18.1`. ([#2319](https://github.com/diffplug/spotless/pull/2319)) +* Bump default `ktfmt` version to latest `0.52` -> `0.53`. ([#2320](https://github.com/diffplug/spotless/pull/2320)) +* Bump default `ktlint` version to latest `1.4.0` -> `1.5.0`. ([#2354](https://github.com/diffplug/spotless/pull/2354)) +* Bump minimum `eclipse-cdt` version to `11.0` (removed support for `10.7`). ([#2373](https://github.com/diffplug/spotless/pull/2373)) +* Bump default `eclipse` version to latest `4.32` -> `4.34`. ([#2381](https://github.com/diffplug/spotless/pull/2381)) + +### Fixed +* You can now use `removeUnusedImports` and `googleJavaFormat` at the same time again. (fixes [#2159](https://github.com/diffplug/spotless/issues/2159)) +* The default list of type annotations used by `formatAnnotations` now includes Jakarta Validation's `Valid` and constraints validations (fixes [#2334](https://github.com/diffplug/spotless/issues/2334)) + + +## [3.0.0.BETA4] - 2024-10-24 +### Added +* APIs to support linting. (implemented in [#2148](https://github.com/diffplug/spotless/pull/2148), [#2149](https://github.com/diffplug/spotless/pull/2149), [#2307](https://github.com/diffplug/spotless/pull/2307)) + * Spotless is still primarily a formatter, not a linter. But when formatting fails, it's more flexible to model those failures as lints so that the formatting can continue and ideally we can also capture the line numbers causing the failure. + * `Lint` models a single change. A `FormatterStep` can create a lint by: + * throwing an exception during formatting, ideally `throw Lint.atLine(127, "code", "Well what happened was...")` + * or by implementing the `List lint(String content, File file)` method to create multiple of them +* Support for line ending policy `PRESERVE` which just takes the first line ending of every given file as setting (no matter if `\n`, `\r\n` or `\r`) ([#2304](https://github.com/diffplug/spotless/pull/2304)) +### Changed +* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148)) + * **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`. +* `FenceStep` now uses `ConfigurationCacheHack`. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317)) +### Fixed +* `ktlint` steps now read from the `string` instead of the `file` so they don't clobber earlier steps. (fixes [#1599](https://github.com/diffplug/spotless/issues/1599)) + +## [3.0.0.BETA3] - 2024-10-15 +### Added +* Support for `rdf` ([#2261](https://github.com/diffplug/spotless/pull/2261)) +* Support for `buf` on maven plugin ([#2291](https://github.com/diffplug/spotless/pull/2291)) +* `ConfigurationCacheHack` so we can support Gradle's configuration cache and remote build cache at the same time. ([#2298](https://github.com/diffplug/spotless/pull/2298) fixes [#2168](https://github.com/diffplug/spotless/issues/2168)) +### Changed +* Support configuring the Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238)) +* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`. + ([#2259](https://github.com/diffplug/spotless/pull/2259)) +* Bump default `buf` version to latest `1.24.0` -> `1.44.0`. ([#2291](https://github.com/diffplug/spotless/pull/2291)) +* Bump default `google-java-format` version to latest `1.23.0` -> `1.24.0`. ([#2294](https://github.com/diffplug/spotless/pull/2294)) +* Bump default `jackson` version to latest `2.17.2` -> `2.18.0`. ([#2279](https://github.com/diffplug/spotless/pull/2279)) +* Bump default `cleanthat` version to latest `2.21` -> `2.22`. ([#2296](https://github.com/diffplug/spotless/pull/2296)) +### Fixed +* Java import order, ignore duplicate group entries. ([#2293](https://github.com/diffplug/spotless/pull/2293)) + +## [3.0.0.BETA2] - 2024-08-25 +### Changed +* Support toning down sortPom logging. ([#2185](https://github.com/diffplug/spotless/pull/2185)) +* Bump default `ktlint` version to latest `1.2.1` -> `1.3.0`. ([#2165](https://github.com/diffplug/spotless/pull/2165)) +* Bump default `ktfmt` version to latest `0.49` -> `0.52`. ([#2172](https://github.com/diffplug/spotless/pull/2172), [#2231](https://github.com/diffplug/spotless/pull/2231)) +* Rename property `ktfmt` option `removeUnusedImport` -> `removeUnusedImports` to match `ktfmt`. ([#2172](https://github.com/diffplug/spotless/pull/2172)) +* Bump default `eclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `greclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179), [#2190](https://github.com/diffplug/spotless/pull/2190)) +* Bump default `cdt` version to latest `11.3` -> `11.6`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `gson` version to latest `2.10.1` -> `2.11.0`. ([#2128](https://github.com/diffplug/spotless/pull/2128)) +* Bump default `jackson` version to latest `2.17.1` -> `2.17.2`. ([#2195](https://github.com/diffplug/spotless/pull/2195)) +* Bump default `cleanthat` version to latest `2.20` -> `2.21`. ([#2210](https://github.com/diffplug/spotless/pull/2210)) +* Bump default `google-java-format` version to latest `1.22.0` -> `1.23.0`. ([#2212](https://github.com/diffplug/spotless/pull/2212)) +### Fixed +* Fix compatibility issue introduced by `ktfmt` `0.51`. ([#2172](https://github.com/diffplug/spotless/issues/2172)) +### Added +* Add option `manageTrailingCommas` to `ktfmt`. ([#2177](https://github.com/diffplug/spotless/pull/2177)) + +## [3.0.0.BETA1] - 2024-06-04 +### Added +* `FileSignature.Promised` and `JarState.Promised` to facilitate round-trip serialization for the Gradle configuration cache. ([#1945](https://github.com/diffplug/spotless/pull/1945)) +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) +### Fixed +* Check if ktlint_code_style is set in .editorconfig before overriding it ([#2143](https://github.com/diffplug/spotless/issues/2143)) +* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990)) +* Correctly provide EditorConfig property types for Ktlint ([#2052](https://github.com/diffplug/spotless/issues/2052)) +* Made ShadowCopy (`npmInstallCache`) more robust by re-creating the cache dir if it goes missing ([#1984](https://github.com/diffplug/spotless/issues/1984),[2096](https://github.com/diffplug/spotless/pull/2096)) +* scalafmt.conf fileOverride section now works correctly ([#1854](https://github.com/diffplug/spotless/pull/1854)) +* Fix stdin pipe is being closed exception on Windows for large .proto files ([#2147](https://github.com/diffplug/spotless/issues/2147)) +* Reworked ShadowCopy (`npmInstallCache`) to use atomic filesystem operations, resolving several race conditions that could arise ([#2151](https://github.com/diffplug/spotless/pull/2151)) +### Changed +* Bump default `cleanthat` version to latest `2.16` -> `2.20`. ([#1725](https://github.com/diffplug/spotless/pull/1725)) +* Bump default `gherkin-utils` version to latest `8.0.2` -> `9.0.0`. ([#1703](https://github.com/diffplug/spotless/pull/1703)) +* Bump default `google-java-format` version to latest `1.19.2` -> `1.22.0`. ([#2129](https://github.com/diffplug/spotless/pull/2129)) +* Bump default `jackson` version to latest `2.14.2` -> `2.17.1`. ([#1685](https://github.com/diffplug/spotless/pull/1685)) +* Bump default `ktfmt` version to latest `0.46` -> `0.49`. ([#2045](https://github.com/diffplug/spotless/pull/2045), [#2127](https://github.com/diffplug/spotless/pull/2127)) +* Bump default `ktlint` version to latest `1.1.1` -> `1.2.1`. ([#2057](https://github.com/diffplug/spotless/pull/2057)) +* Bump default `scalafmt` version to latest `3.7.3` -> `3.8.1`. ([#1730](https://github.com/diffplug/spotless/pull/1730)) +* Bump default `shfmt` version to latest `3.7.0` -> `3.8.0`. ([#2050](https://github.com/diffplug/spotless/pull/2050)) +* Bump default `sortpom` version to latest `3.2.1` -> `4.0.0`. ([#2049](https://github.com/diffplug/spotless/pull/2049), [#2078](https://github.com/diffplug/spotless/pull/2078), [#2115](https://github.com/diffplug/spotless/pull/2115)) +* Bump default `zjsonpatch` version to latest `0.4.14` -> `0.4.16`. ([#1969](https://github.com/diffplug/spotless/pull/1969)) +### Removed +* **BREAKING** Remove `FormatterStep.createNeverUpToDate` methods, they are available only in `testlib`. ([#2145](https://github.com/diffplug/spotless/pull/2145)) +* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945)) +* **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954)) +* **BREAKING** Fully removed `Rome`, use `Biome` instead. ([#2119](https://github.com/diffplug/spotless/pull/2119)) +* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148)) + * **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`. + +## [2.45.0] - 2024-01-23 +### Added +* Support for `gofmt` ([#2001](https://github.com/diffplug/spotless/pull/2001)) +* Support for formatting Java Docs for the Palantir formatter ([#2009](https://github.com/diffplug/spotless/pull/2009)) + +## [2.44.0] - 2024-01-15 +### Added +* New static method to `DiffMessageFormatter` which allows to retrieve diffs with their line numbers ([#1960](https://github.com/diffplug/spotless/issues/1960)) +* Gradle - Support for formatting shell scripts via [shfmt](https://github.com/mvdan/sh). ([#1994](https://github.com/diffplug/spotless/pull/1994)) +### Fixed +* Fix empty files with biome >= 1.5.0 when formatting files that are in the ignore list of the biome configuration file. ([#1989](https://github.com/diffplug/spotless/pull/1989) fixes [#1987](https://github.com/diffplug/spotless/issues/1987)) +* Fix a regression in BufStep where the same arguments were being provided to every `buf` invocation. ([#1976](https://github.com/diffplug/spotless/issues/1976)) +### Changed +* Use palantir-java-format 2.39.0 on Java 21. ([#1948](https://github.com/diffplug/spotless/pull/1948)) +* Bump default `ktlint` version to latest `1.0.1` -> `1.1.1`. ([#1973](https://github.com/diffplug/spotless/pull/1973)) +* Bump default `googleJavaFormat` version to latest `1.18.1` -> `1.19.2`. ([#1971](https://github.com/diffplug/spotless/pull/1971)) +* Bump default `diktat` version to latest `1.2.5` -> `2.0.0`. ([#1972](https://github.com/diffplug/spotless/pull/1972)) + +## [2.43.1] - 2023-12-04 +### Fixed +* Eclipse-based steps which contained any jars with a `+` in their path were broken, now fixed. ([#1860](https://github.com/diffplug/spotless/issues/1860#issuecomment-1826113332)) +### Changed +* Bump default `palantir-java-format` version to latest `2.28.0` -> `2.38.0` on Java 21. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `googleJavaFormat` version to latest `1.17.0` -> `1.18.1`. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `ktfmt` version to latest `0.44` -> `0.46`. ([#1927](https://github.com/diffplug/spotless/pull/1927)) +* Bump default `eclipse` version to latest `4.27` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `greclipse` version to latest `4.28` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `cdt` version to latest `11.1` -> `11.3`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) + +## [2.43.0] - 2023-11-27 +### Added +* Support custom rule sets for Ktlint. ([#1896](https://github.com/diffplug/spotless/pull/1896)) +### Fixed +* Fix Eclipse JDT on some settings files. ([#1864](https://github.com/diffplug/spotless/pull/1864) fixes [#1638](https://github.com/diffplug/spotless/issues/1638)) +### Changed +* Bump default `ktlint` version to latest `1.0.0` -> `1.0.1`. ([#1855](https://github.com/diffplug/spotless/pull/1855)) +* Add a Step to remove semicolons from Groovy files. ([#1881](https://github.com/diffplug/spotless/pull/1881)) + +## [2.42.0] - 2023-09-28 +### Added +* Support for biome. The Rome project [was renamed to Biome](https://biomejs.dev/blog/annoucing-biome/). + The configuration is still the same, but you should switch to the new `biome` tag / function and adjust + the version accordingly. ([#1804](https://github.com/diffplug/spotless/issues/1804)). +* Support for `google-java-format`'s `skip-javadoc-formatting` option. ([#1793](https://github.com/diffplug/spotless/pull/1793)) +* Support configuration of mirrors for P2 repositories in Maven DSL ([#1697](https://github.com/diffplug/spotless/issues/1697)). +* New line endings mode `GIT_ATTRIBUTES_FAST_ALLSAME`. ([#1838](https://github.com/diffplug/spotless/pull/1838)) +### Fixed +* Fix support for plugins when using Prettier version `3.0.0` and newer. ([#1802](https://github.com/diffplug/spotless/pull/1802)) +* Fix configuration cache issue around `external process started '/usr/bin/git --version'`. ([#1806](https://github.com/diffplug/spotless/issues/1806)) +### Changed +* Bump default `flexmark` version to latest `0.64.0` -> `0.64.8`. ([#1801](https://github.com/diffplug/spotless/pull/1801)) +* Bump default `ktlint` version to latest `0.50.0` -> `1.0.0`. ([#1808](https://github.com/diffplug/spotless/pull/1808)) + +## [2.41.0] - 2023-08-29 +### Added +* Add a `jsonPatch` step to `json` formatter configurations. This allows patching of JSON documents using [JSON Patches](https://jsonpatch.com). ([#1753](https://github.com/diffplug/spotless/pull/1753)) +* Support GJF own import order. ([#1780](https://github.com/diffplug/spotless/pull/1780)) +### Fixed +* Use latest versions of popular style guides for `eslint` tests to fix failing `useEslintXoStandardRules` test. ([#1761](https://github.com/diffplug/spotless/pull/1761), [#1756](https://github.com/diffplug/spotless/issues/1756)) +* Add support for `prettier` version `3.0.0` and newer. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1751](https://github.com/diffplug/spotless/issues/1751)) +* Fix npm install calls when npm cache is not up-to-date. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1750](https://github.com/diffplug/spotless/issues/1750)) +### Changed +* Bump default `eslint` version to latest `8.31.0` -> `8.45.0` ([#1761](https://github.com/diffplug/spotless/pull/1761)) +* Bump default `prettier` version to latest (v2) `2.8.1` -> `2.8.8`. ([#1760](https://github.com/diffplug/spotless/pull/1760)) +* Bump default `greclipse` version to latest `4.27` -> `4.28`. ([#1775](https://github.com/diffplug/spotless/pull/1775)) + +## [2.40.0] - 2023-07-17 +### Added +* Added support for Protobuf formatting based on [Buf](https://buf.build/). (#1208) +* `enum OnMatch { INCLUDE, EXCLUDE }` so that `FormatterStep.filterByContent` can not only include based on the pattern but also exclude. ([#1749](https://github.com/diffplug/spotless/pull/1749)) +### Fixed +* Update documented default `semanticSort` to `false`. ([#1728](https://github.com/diffplug/spotless/pull/1728)) +### Changed +* Bump default `cleanthat` version to latest `2.13` -> `2.17`. ([#1734](https://github.com/diffplug/spotless/pull/1734)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`. ([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. + * Dropped support for deprecated `useExperimental` parameter in favor of the `ktlint_experimental` property. + +## [2.39.0] - 2023-05-24 +### Added +* `Jvm.Support` now accepts `-SNAPSHOT` versions, treated as the non`-SNAPSHOT`. ([#1583](https://github.com/diffplug/spotless/issues/1583)) +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) +* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522)) +### Fixed +* Fixed a regression which changed the import sorting order in `googleJavaFormat` introduced in `2.38.0`. ([#1680](https://github.com/diffplug/spotless/pull/1680)) +* Equo-based formatters now work on platforms unsupported by Eclipse such as PowerPC (fixes [durian-swt#20](https://github.com/diffplug/durian-swt/issues/20)) +* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) +### Changed +* Equo-based formatters now download metadata to `~/.m2/repository/dev/equo/p2-data` rather than `~/.equo`, and for CI machines without a home directory the p2 data goes to `$GRADLE_USER_HOME/caches/p2-data`. ([#1714](https://github.com/diffplug/spotless/pull/1714)) +* Bump default `googleJavaFormat` version to latest `1.16.0` -> `1.17.0`. ([#1710](https://github.com/diffplug/spotless/pull/1710)) +* Bump default `ktfmt` version to latest `0.43` -> `0.44`. ([#1691](https://github.com/diffplug/spotless/pull/1691)) +* Bump default `ktlint` version to latest `0.48.2` -> `0.49.1`. ([#1696](https://github.com/diffplug/spotless/issues/1696)) + * Dropped support for `ktlint 0.46.x` following our policy of supporting two breaking changes at a time. +* Bump default `sortpom` version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) + +## [2.38.0] - 2023-04-06 +### Added +* Support configuration of mirrors for P2 repositories in `EquoBasedStepBuilder` ([#1629](https://github.com/diffplug/spotless/issues/1629)). +* The `style` option in Palantir Java Format ([#1654](https://github.com/diffplug/spotless/pull/1654)). +* Added formatter for Gherkin feature files ([#1649](https://github.com/diffplug/spotless/issues/1649)). +### Changed +* **POTENTIALLY BREAKING** Converted `googleJavaFormat` to a compile-only dependency and drop support for versions < `1.8`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `cleanthat` version to latest `2.6` -> `2.13`. ([#1589](https://github.com/diffplug/spotless/pull/1589) and [#1661](https://github.com/diffplug/spotless/pull/1661)) +* Bump default `diktat` version `1.2.4.2` -> `1.2.5`. ([#1631](https://github.com/diffplug/spotless/pull/1631)) +* Bump default `flexmark` version `0.62.2` -> `0.64.0`. ([#1302](https://github.com/diffplug/spotless/pull/1302)) +* Bump default `googleJavaFormat` version `1.15.0` -> `1.16.0`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `scalafmt` version `3.7.1` -> `3.7.3`. ([#1584](https://github.com/diffplug/spotless/pull/1584)) +* Bump default Eclipse formatters for the 2023-03 release. ([#1662](https://github.com/diffplug/spotless/pull/1662)) + * JDT and GrEclipse `4.26` -> `4.27` + * Improve GrEclipse error reporting. ([#1660](https://github.com/diffplug/spotless/pull/1660)) + * CDT `11.0` -> `11.1` + +## [2.37.0] - 2023-03-13 +### Added +* You can now put the filename into a license header template with `$FILE`. ([#1605](https://github.com/diffplug/spotless/pull/1605) fixes [#1147](https://github.com/diffplug/spotless/issues/1147)) +### Changed +* We are now opting in to Gradle's new stable configuration cache. ([#1591](https://github.com/diffplug/spotless/pull/1591)) +* Adopt [Equo Solstice OSGi and p2 shim](https://github.com/equodev/equo-ide/tree/main/solstice) to update all Eclipse-based plugins. ([#1524](https://github.com/diffplug/spotless/pull/1524)) + * Eclipse JDT now supports `4.9` through `4.26`. Also we now recommend dropping the last `.0`, e.g. `4.26` instead of `4.26.0`, you'll get warnings to help you switch. + * Eclipse Groovy now supports `4.18` through `4.26`. Also we now recommend dropping the last `.0`, e.g. `4.26` instead of `4.26.0`, you'll get warnings to help you switch. + * Eclipse CDT now supports `10.6` through `11.0`. + * Eclipse WTP is still WIP at [#1622](https://github.com/diffplug/spotless/pull/1622). + +## [2.36.0] - 2023-02-27 +### Added +* `gradlew equoIde` opens a repeatable clean Spotless dev environment. ([#1523](https://github.com/diffplug/spotless/pull/1523)) +* `cleanthat` added `includeDraft` option, to include draft mutators from composite mutators. ([#1574](https://github.com/diffplug/spotless/pull/1574)) +* `npm`-based formatters now support caching of `node_modules` directory ([#1590](https://github.com/diffplug/spotless/pull/1590)) +### Fixed +* `JacksonJsonFormatterFunc` handles json files with an Array as root. ([#1585](https://github.com/diffplug/spotless/pull/1585)) +### Changed +* Bump default `cleanthat` version to latest `2.1` -> `2.6` ([#1569](https://github.com/diffplug/spotless/pull/1569) and [#1574](https://github.com/diffplug/spotless/pull/1574)) +* Reduce logging-noise created by `npm`-based formatters ([#1590](https://github.com/diffplug/spotless/pull/1590) fixes [#1582](https://github.com/diffplug/spotless/issues/1582)) + +## [2.35.0] - 2023-02-10 +### Added +* CleanThat Java Refactorer. ([#1560](https://github.com/diffplug/spotless/pull/1560)) +* Introduce `LazyArgLogger` to allow for lazy evaluation of log messages in slf4j logging. ([#1565](https://github.com/diffplug/spotless/pull/1565)) +### Fixed +* Allow multiple instances of the same npm-based formatter to be used by separating their `node_modules` directories. ([#1565](https://github.com/diffplug/spotless/pull/1565)) +* `ktfmt` default style uses correct continuation indent. ([#1562](https://github.com/diffplug/spotless/pull/1562)) +### Changed +* Bump default `ktfmt` version to latest `0.42` -> `0.43` ([#1561](https://github.com/diffplug/spotless/pull/1561)) +* Bump default `jackson` version to latest `2.14.1` -> `2.14.2` ([#1536](https://github.com/diffplug/spotless/pull/1536)) + +## [2.34.1] - 2023-02-05 +### Changed +* **POTENTIALLY BREAKING** Bump bytecode from Java 8 to 11 ([#1530](https://github.com/diffplug/spotless/pull/1530) part 2 of [#1337](https://github.com/diffplug/spotless/issues/1337)) +### Fixed +* **POTENTIALLY BREAKING** `sortByKeys` for JSON formatting now takes into account objects inside arrays ([#1546](https://github.com/diffplug/spotless/pull/1546)) +* `freshmark` fixed on java 15+ ([#1304](https://github.com/diffplug/spotless/pull/1304) fixes [#803](https://github.com/diffplug/spotless/issues/803)) + +## [2.34.0] - 2023-01-26 +### Added +* `Formatter` now has a field `public static final File NO_FILE_SENTINEL` which can be used to pass string content to a Formatter or FormatterStep when there is no actual File to format. ([#1525](https://github.com/diffplug/spotless/pull/1525)) + +## [2.33.0] - 2023-01-26 +### Added +* `ProcessRunner` has added some convenience methods so it can be used for Maven testing. ([#1496](https://github.com/diffplug/spotless/pull/1496)) +* `ProcessRunner` allows to limit captured output to a certain number of bytes. ([#1511](https://github.com/diffplug/spotless/pull/1511)) +* `ProcessRunner` is now capable of handling long-running tasks where waiting for exit is delegated to the caller. ([#1511](https://github.com/diffplug/spotless/pull/1511)) +* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500)) +### Fixed +* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494) +### Changed +* **POTENTIALLY BREAKING** Bump minimum JRE from 8 to 11, next release likely to bump bytecode to Java 11 ([#1514](https://github.com/diffplug/spotless/pull/1514) part 1 of [#1337](https://github.com/diffplug/spotless/issues/1337)) +* Rename `YamlJacksonStep` into `JacksonYamlStep` while normalizing Jackson usage ([#1492](https://github.com/diffplug/spotless/pull/1492)) +* Convert `gson` integration to use a compile-only source set ([#1510](https://github.com/diffplug/spotless/pull/1510)). +* ** POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) + * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. + * From now on, we will support no more than 2 breaking changes at a time. +* NpmFormatterStepStateBase delays `npm install` call until the formatter is first used. This enables better integration + with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522)) +* Bump default `ktlint` version to latest `0.48.1` -> `0.48.2` ([#1529](https://github.com/diffplug/spotless/pull/1529)) +* Bump default `scalafmt` version to latest `3.6.1` -> `3.7.1` ([#1529](https://github.com/diffplug/spotless/pull/1529)) + +## [2.32.0] - 2023-01-13 +### Added +* Add option `editorConfigFile` for `ktLint` [#142](https://github.com/diffplug/spotless/issues/142) + * **POTENTIALLY BREAKING** `ktlint` step now modifies license headers. Make sure to put `licenseHeader` *after* `ktlint`. +* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441)). +* Add YAML support through Jackson ([#1478](https://github.com/diffplug/spotless/pull/1478)) +* Added support for npm-based [ESLint](https://eslint.org/)-formatter for javascript and typescript ([#1453](https://github.com/diffplug/spotless/pull/1453)) +* Better suggested messages when user's default is set by JVM limitation. ([#995](https://github.com/diffplug/spotless/pull/995)) +### Fixed +* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444)) +* Fix subgroups leading catch all matcher. +### Changed +* Bump default version for `prettier` from `2.0.5` to `2.8.1` ([#1453](https://github.com/diffplug/spotless/pull/1453)) +* Bump the dev version of Gradle from `7.5.1` to `7.6` ([#1409](https://github.com/diffplug/spotless/pull/1409)) + * We also removed the no-longer-required dependency `org.codehaus.groovy:groovy-xml` +* Breaking changes to Spotless' internal testing infrastructure `testlib` ([#1443](https://github.com/diffplug/spotless/pull/1443)) + * `ResourceHarness` no longer has any duplicated functionality which was also present in `StepHarness` + * `StepHarness` now operates on `Formatter` rather than a `FormatterStep` + * `StepHarnessWithFile` now takes a `ResourceHarness` in its constructor to handle the file manipulation parts + * Standardized that we test exception *messages*, not types, which will ease the transition to linting later on + * Bump default `ktlint` version to latest `0.47.1` -> `0.48.1` ([#1456](https://github.com/diffplug/spotless/pull/1456)) +* Switch our publishing infrastructure from CircleCI to GitHub Actions ([#1462](https://github.com/diffplug/spotless/pull/1462)). + * Help wanted for moving our tests too ([#1472](https://github.com/diffplug/spotless/issues/1472)) + +## [2.31.1] - 2023-01-02 +### Fixed +* Improve memory usage when using git ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426)) +* Support `ktlint` 0.48+ ([#1432](https://github.com/diffplug/spotless/pull/1432)) fixes ([#1430](https://github.com/diffplug/spotless/issues/1430)) +### Changed +* Bump default `ktlint` version to latest `0.47.1` -> `0.48.0` ([#1432](https://github.com/diffplug/spotless/pull/1432)) +* Bump default `ktfmt` version to latest `0.41` -> `0.42` ([#1421](https://github.com/diffplug/spotless/pull/1421)) + +## [2.31.0] - 2022-11-24 +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) +### Fixed +* Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) +* Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) +* Share git repositories across projects when using ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426)) +### Changed +* Bump default `ktfmt` version to latest `0.40` -> `0.41` ([#1340](https://github.com/diffplug/spotless/pull/1340)) +* Bump default `scalafmt` version to latest `3.5.9` -> `3.6.1` ([#1373](https://github.com/diffplug/spotless/pull/1373)) +* Bump default `diktat` version to latest `1.2.3` -> `1.2.4.2` ([#1393](https://github.com/diffplug/spotless/pull/1393)) +* Bump default `palantir-java-format` version to latest `2.10` -> `2.28` ([#1393](https://github.com/diffplug/spotless/pull/1393)) + +## [2.30.0] - 2022-09-14 +### Added +* `formatAnnotations()` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat()`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) +### Changed +* Bump default `ktfmt` version to latest `0.39` -> `0.40` ([#1312](https://github.com/diffplug/spotless/pull/1312)) +* Bump default `ktlint` version to latest `0.46.1` -> `0.47.1` ([#1303](https://github.com/diffplug/spotless/pull/1303)) + * Also restored support for older versions of ktlint back to `0.31.0` + +## [2.29.0] - 2022-08-23 +### Added +* `scalafmt` integration now has a configuration option `majorScalaVersion` that allows you to configure the Scala version that gets resolved from the Maven artifact ([#1283](https://github.com/diffplug/spotless/pull/1283)) + * Converted `scalafmt` integration to use a compile-only source set (fixes [#524](https://github.com/diffplug/spotless/issues/524)) +### Changed +* Add the `ktlint` rule in error messages when `ktlint` fails to apply a fix ([#1279](https://github.com/diffplug/spotless/pull/1279)) +* Bump default `scalafmt` to latest `3.0.8` -> `3.5.9` (removed support for pre-`3.0.0`) ([#1283](https://github.com/diffplug/spotless/pull/1283)) + +## [2.28.1] - 2022-08-10 +### Fixed +* Fix Clang not knowing the filename and changing the format ([#1268](https://github.com/diffplug/spotless/pull/1268) fixes [#1267](https://github.com/diffplug/spotless/issues/1267)). +### Changed +* Bump default `diktat` version to latest `1.2.1` -> `1.2.3` ([#1266](https://github.com/diffplug/spotless/pull/1266)) + +## [2.28.0] - 2022-07-28 +### Added +* Clang and Black no longer break the build when the binary is unavailable, if they will not be run during that build ([#1257](https://github.com/diffplug/spotless/pull/1257)). +* License header support for Kotlin files without `package` or `@file` but do at least have `import` ([#1263](https://github.com/diffplug/spotless/pull/1263)). + +## [2.27.0] - 2022-06-30 +### Added +* Support for `MAC_CLASSIC` (`\r`) line ending ([#1243](https://github.com/diffplug/spotless/pull/1243) fixes [#1196](https://github.com/diffplug/spotless/issues/1196)) +### Changed +* Bump default `ktlint` version to latest `0.45.2` -> `0.46.1` ([#1239](https://github.com/diffplug/spotless/issues/1239)) + * Minimum supported version also bumped to `0.46.0` (we have abandoned strong backward compatibility for `ktlint`, from here on out Spotless will only support the most-recent breaking change). +* Bump default `diktat` version to latest `1.1.0` -> `1.2.1` ([#1246](https://github.com/diffplug/spotless/pull/1246)) + * Minimum supported version also bumped to `1.2.1` (diktat is based on ktlint and has the same backward compatibility issues). +* Bump default `ktfmt` version to latest `0.37` -> `0.39` ([#1240](https://github.com/diffplug/spotless/pull/1240)) + +## [2.26.2] - 2022-06-11 +### Fixed +* `PalantirJavaFormatStep` no longer needs the `--add-exports` calls in the `org.gradle.jvmargs` property in `gradle.properties`. ([#1233](https://github.com/diffplug/spotless/pull/1233)) + +## [2.26.1] - 2022-06-10 +### Fixed +* (Second try) `googleJavaFormat` and `removeUnusedImports` works on JDK16+ without jvm args workaround. ([#1228](https://github.com/diffplug/spotless/pull/1228)) + +## [2.26.0] - 2022-06-05 +### Added +* Support for `editorConfigOverride` in `ktlint`. ([#1218](https://github.com/diffplug/spotless/pull/1218) fixes [#1193](https://github.com/diffplug/spotless/issues/1193)) +### Fixed +* `google-java-format` and `RemoveUnusedImportsStep` works on JDK16+ without jvm args workaround. ([#1224](https://github.com/diffplug/spotless/pull/1224) fixes [#834](https://github.com/diffplug/spotless/issues/834)) + +## [2.25.3] - 2022-05-10 +### Fixed +* Update the `black` version regex to fix `19.10b0` and earlier. (fixes [#1195](https://github.com/diffplug/spotless/issues/1195), regression introduced in `2.25.0`) +* `GitAttributesLineEndings$RelocatablePolicy` and `FormatterStepImpl` now null-out their initialization lambdas after their state has been calculated, which allows GC to collect variables which were incidentally captured but not needed in the calculated state. ([#1198](https://github.com/diffplug/spotless/pull/1198)) +### Changed +* Bump default `ktfmt` version to latest `0.36` -> `0.37`. ([#1200](https://github.com/diffplug/spotless/pull/1200)) + +## [2.25.2] - 2022-05-03 +### Changed +* Bump default `diktat` version to latest `1.0.1` -> `1.1.0`. ([#1190](https://github.com/diffplug/spotless/pull/1190)) + * Converted `diktat` integration to use a compile-only source set. (fixes [#524](https://github.com/diffplug/spotless/issues/524)) + * Use the full path to a file in `diktat` integration. (fixes [#1189](https://github.com/diffplug/spotless/issues/1189)) + +## [2.25.1] - 2022-04-27 +### Changed +* Bump default `ktfmt` version to latest `0.35` -> `0.36`. ([#1183](https://github.com/diffplug/spotless/issues/1183)) +* Bump default `google-java-format` version to latest `1.13.0` -> `1.15.0`. + * ~~This means it is no longer necessary to use the `--add-exports` workaround (fixes [#834](https://github.com/diffplug/spotless/issues/834)).~~ `--add-exports` workaround is still needed. + +## [2.25.0] - 2022-04-22 +### Added +* Added support for enabling ktlint experimental ruleset. ([#1145](https://github.com/diffplug/spotless/pull/1168)) +### Fixed +* Fixed support for Python Black's new version reporting. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* Error messages for unexpected file encoding now works on Java 8. (fixes [#1081](https://github.com/diffplug/spotless/issues/1081)) +### Changed +* Bump default `black` version to latest `19.10b0` -> `22.3.0`. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* Bump default `ktfmt` version to latest `0.34` -> `0.35`. ([#1159](https://github.com/diffplug/spotless/pull/1159)) +* Bump default `ktlint` version to latest `0.43.2` -> `0.45.2`. ([#1177](https://github.com/diffplug/spotless/pull/1177)) + +## [2.24.2] - 2022-04-06 +### Fixed +* Git user config and system config also included for defaultEndings configuration. ([#540](https://github.com/diffplug/spotless/issues/540)) + +## [2.24.1] - 2022-03-30 +### Fixed +* Fixed access modifiers for setters in KtfmtStep configuration + +## [2.24.0] - 2022-03-28 +### Added +* Added support for setting custom parameters for Kotlin ktfmt in Gradle and Maven plugins. ([#1145](https://github.com/diffplug/spotless/pull/1145)) + +## [2.23.0] - 2022-02-15 +### Added +* Added support for JSON formatting based on [Gson](https://github.com/google/gson) ([#1125](https://github.com/diffplug/spotless/pull/1125)). +### Changed +* Use SLF4J for logging ([#1116](https://github.com/diffplug/spotless/issues/1116)) + +## [2.22.2] - 2022-02-09 +### Changed +* Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). + +## [2.22.1] - 2022-02-01 ### Changed * Bump CI from Java 15 to 17 ([#1094](https://github.com/diffplug/spotless/pull/1094)). * Bump default versions of formatters ([#1095](https://github.com/diffplug/spotless/pull/1095)). * google-java-format `1.12.0` -> `1.13.0` * ktfmt `0.29` -> `0.30` +* Added support for git property `core.autocrlf` ([#540](https://github.com/diffplug/spotless/issues/540)) ## [2.22.0] - 2022-01-13 ### Added @@ -138,7 +585,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * Update ktfmt from 0.21 to 0.24 ### Fixed -* The `` field in the maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) +* The `` field in the Maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) * Node is re-installed if some other build step removed it ([#863](https://github.com/diffplug/spotless/issues/863)) ## [2.13.4] - 2021-04-21 @@ -212,7 +659,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [2.8.0] - 2020-10-05 ### Added -* Exposed new methods in `GitRatchet` to support faster ratcheting in the maven plugin ([#706](https://github.com/diffplug/spotless/pull/706)). +* Exposed new methods in `GitRatchet` to support faster ratcheting in the Maven plugin ([#706](https://github.com/diffplug/spotless/pull/706)). ## [2.7.0] - 2020-09-21 ### Added @@ -283,10 +730,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `FormatterFunc.NeedsFile` for implementing file-based formatters more cleanly than we have so far ([#637](https://github.com/diffplug/spotless/issues/637)). ### Changed * **BREAKING** `FileSignature` can no longer sign folders, only files. Signatures are now based only on filename (not path), size, and a content hash. It throws an error if a signature is attempted on a folder or on multiple files with different paths but the same filename - it never breaks silently. This change does not break any of Spotless' internal logic, so it is unlikely to affect any of Spotless' consumers either. ([#571](https://github.com/diffplug/spotless/pull/571)) - * This change allows the maven plugin to cache classloaders across subprojects when loading config resources from the classpath (fixes [#559](https://github.com/diffplug/spotless/issues/559)). - * This change also allows the gradle plugin to work with the remote buildcache (fixes [#280](https://github.com/diffplug/spotless/issues/280)). + * This change allows the Maven plugin to cache classloaders across subprojects when loading config resources from the classpath (fixes [#559](https://github.com/diffplug/spotless/issues/559)). + * This change also allows the Gradle plugin to work with the remote buildcache (fixes [#280](https://github.com/diffplug/spotless/issues/280)). * **BREAKING** `FormatterFunc` no longer `extends ThrowingEx.Function` and `BiFunction`. In a major win for Java's idea of ["target typing"](https://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html), this required no changes anywhere in the codebase except deleting the `extends` part of `FormatterFunc` ([#638](https://github.com/diffplug/spotless/issues/638)). -* **BREAKING** Heavy refactor of the `LicenseHeaderStep` public API. Doesn't change internal behavior, but makes implementation of the gradle and maven plugins much easier. ([#628](https://github.com/diffplug/spotless/pull/628)) +* **BREAKING** Heavy refactor of the `LicenseHeaderStep` public API. Doesn't change internal behavior, but makes implementation of the Gradle and Maven plugins much easier. ([#628](https://github.com/diffplug/spotless/pull/628)) * **BREAKING** Removed all deprecated methods and classes from `lib` and `lib-extra`. * [#629](https://github.com/diffplug/spotless/pull/629) removes the code which wasn't being used in plugin-gradle or plugin-maven. * [#630](https://github.com/diffplug/spotless/pull/630) moves the code which was still being used in deprecated parts of plugin-gradle into `.deprecated` package in `plugin-gradle`, which allows us to break `lib` without a breaking change in `plugin-gradle`. @@ -319,7 +766,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `NodeJsGlobal.setSharedLibFolder` allows to set the location of nodejs shared libs. ([#586](https://github.com/diffplug/spotless/pull/586)) * `PaddedCell.isClean()` returns the instance of `PaddedCell.DirtyState` which represents clean. ([#590](https://github.com/diffplug/spotless/pull/590)) ### Fixed -* Previously, the nodejs-based steps would throw `UnsatisfiedLinkError` if they were ever used from more than one classloader. Now they can be used from any number of classloaders (important for gradle build daemon). ([#586](https://github.com/diffplug/spotless/pull/586)) +* Previously, the nodejs-based steps would throw `UnsatisfiedLinkError` if they were ever used from more than one classloader. Now they can be used from any number of classloaders (important for Gradle build daemon). ([#586](https://github.com/diffplug/spotless/pull/586)) ## [1.31.0] - 2020-05-21 ### Added @@ -341,8 +788,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * Updated a bunch of dependencies, most notably: ([#564](https://github.com/diffplug/spotless/pull/564)) * jgit `5.5.0.201909110433-r` -> `5.7.0.202003110725-r` - * gradle `6.2.2` -> `6.3` - * spotbugs gradle plugin `2.0.0` -> `4.0.8` + * Gradle `6.2.2` -> `6.3` + * spotbugs Gradle plugin `2.0.0` -> `4.0.8` ## [1.28.1] - 2020-04-02 ### Fixed @@ -359,12 +806,12 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * We now use [javadoc.io](https://javadoc.io/) instead of github pages. ([#508](https://github.com/diffplug/spotless/pull/508)) * We no longer publish `-SNAPSHOT` for every build to `main`, since we have good [JitPack integration](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#gradle---any-commit-in-a-public-github-repo-this-one-or-any-fork). ([#508](https://github.com/diffplug/spotless/pull/508)) * Improved how we use Spotless on itself. ([#509](https://github.com/diffplug/spotless/pull/509)) -* Fix build warnings when building on Gradle 6+, bump build gradle to 6.2.2, and fix javadoc links. ([#536](https://github.com/diffplug/spotless/pull/536)) +* Fix build warnings when building on Gradle 6+, bump build Gradle to 6.2.2, and fix javadoc links. ([#536](https://github.com/diffplug/spotless/pull/536)) ## [1.27.0] - 2020-01-01 * Ignored `KtLintStepTest`, because [gradle/gradle#11752](https://github.com/gradle/gradle/issues/11752) is causing too many CI failures. ([#499](https://github.com/diffplug/spotless/pull/499)) * Also fixed a minor problem in TestProvisioner. -* If you set the environment variable `SPOTLESS_EXCLUDE_MAVEN=true` then the maven plugin will be excluded from the build. ([#502](https://github.com/diffplug/spotless/pull/502)) +* If you set the environment variable `SPOTLESS_EXCLUDE_MAVEN=true` then the Maven plugin will be excluded from the build. ([#502](https://github.com/diffplug/spotless/pull/502)) * We have set this in JitPack, as a workaround for [jitpack/jitpack.io#4112](https://github.com/jitpack/jitpack.io/issues/4112) * Deprecated `SpotlessCache.clear()` in favor of the new `SpotlessCache.clearOnce(Object key)`. ([#501](https://github.com/diffplug/spotless/issues/#501)) @@ -399,7 +846,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - but incorrectly marked as good by "check" - this led to "check" says all good, but then "apply" still causes format (https://github.com/diffplug/spotless/issues/453) - combined with up-to-date checking, could lead to even more confusing results (https://github.com/diffplug/spotless/issues/338) - - only affects the gradle plugin, since that was the only plugin to use this feature + - only affects the Gradle plugin, since that was the only plugin to use this feature * Minor change to `TestProvisioner`, which should fix the cache-breaking issues, allowing us to speed-up the CI builds a bit. * Bumped `scalafmt` default version from `1.1.0` to `2.0.1`, since there are [bugs](https://github.com/diffplug/spotless/issues/454) in the old default ([#458](https://github.com/diffplug/spotless/pull/458)). @@ -412,11 +859,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * Updated default eclipse-jdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * Updated default eclipse-cdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * **KNOWN BUG - accidentally published CDT 9.7 rather than 9.8 - fixed in 1.26.0** -* Added new maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) +* Added new Maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) ## [1.23.1] - 2019-06-17 * Fixes incorrect M2 cache directory path handling of Eclipse based formatters ([#401](https://github.com/diffplug/spotless/issues/401)) -* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) +* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because Gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) ## [1.23.0] - 2019-04-24 * Updated default ktlint from 0.21.0 to 0.32.0, and Maven coords to com.pinterest ([#394](https://github.com/diffplug/spotless/pull/394)) @@ -492,7 +939,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [1.12.0] - 2018-05-14 * Fixed a bug in `LicenseHeaderStep` which caused an exception with some malformed date-aware licenses. ([#222](https://github.com/diffplug/spotless/pull/222)) * Updated default ktlint from 0.14.0 to 0.21.0 -* Add ability to pass custom options to ktlint in gradle plugin. See plugin-gradle/README for details. +* Add ability to pass custom options to ktlint in Gradle plugin. See plugin-gradle/README for details. ## [1.11.0] - 2018-02-26 * Added default indentation of `4` to `IndentStep`. ([#209](https://github.com/diffplug/spotless/pull/209)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94bef92f01..0ee619f680 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Spotless -Pull requests are welcome, preferably against `main`. Feel free to develop spotless any way you like. +Pull requests are welcome, preferably against `main`. Feel free to develop spotless any way you like, but if you like Eclipse and Gradle Buildship then [`gradlew equoIde` will install an IDE and set it up for you](https://github.com/equodev/equo-ide). ## How Spotless works @@ -10,24 +10,23 @@ In order to use and combine `FormatterStep`, you first create a `Formatter`, whi - an encoding - a list of `FormatterStep` -- a line endings policy (`LineEnding.GIT_ATTRIBUTES` is almost always the best choice) +- a line endings policy (`LineEnding.GIT_ATTRIBUTES_FAST_ALLSAME` is almost always the best choice) -Once you have an instance of `Formatter`, you can call `boolean isClean(File)`, or `void applyTo(File)` to either check or apply formatting to a file. Spotless will then: +Once you have an instance of `Formatter`, you can call `DirtyState.of(Formatter, File)`. Under the hood, Spotless will: - parse the raw bytes into a String according to the encoding - normalize its line endings to `\n` - pass the unix string to each `FormatterStep` one after the other +- check for idempotence problems, and repeatedly apply the steps until the [result is stable](PADDEDCELL.md). - apply line endings according to the policy You can also use lower-level methods like `String compute(String unix, File file)` if you'd like to do lower-level processing. All `FormatterStep` implement `Serializable`, `equals`, and `hashCode`, so build systems that support up-to-date checks can easily and correctly determine if any actions need to be taken. -Spotless also provides `PaddedCell`, which makes it easy to diagnose and correct idempotence problems. - ## Project layout -For the folders below in monospace text, they are published on maven central at the coordinate `com.diffplug.spotless:spotless-${FOLDER_NAME}`. The other folders are dev infrastructure. +For the folders below in monospace text, they are published on MavenCentral at the coordinate `com.diffplug.spotless:spotless-${FOLDER_NAME}`. The other folders are dev infrastructure. | Folder | Description | | ------ | ----------- | @@ -36,19 +35,19 @@ For the folders below in monospace text, they are published on maven central at | `lib-extra` | Contains the optional parts of Spotless which require external dependencies. `LineEnding.GIT_ATTRIBUTES` won't work unless `lib-extra` is available. | | `plugin-gradle` | Integrates spotless and all of its formatters into Gradle. | | `plugin-maven` | Integrates spotless and all of its formatters into Maven. | -| _ext | Folder for generating glue jars (specifically packaging Eclipse jars from p2 for consumption using maven). ## How to add a new FormatterStep -The easiest way to create a FormatterStep is `FormatterStep createNeverUpToDate(String name, FormatterFunc function)`, which you can use like this: +The easiest way to create a FormatterStep is to just create `class FooStep implements FormatterStep`. It has one abstract method which is the formatting function, and you're ready to tinker. To work with the build plugins, this class will need to -```java -FormatterStep identityStep = FormatterStep.createNeverUpToDate("identity", unixStr -> unixStr) -``` +- implement equality and hashcode +- support lossless roundtrip serialization + +You can use `StepHarness` (if you don't care about the `File` argument) or `StepHarnessWithFile` to test. The harness will roundtrip serialize your step, check that it's equal to itself, and then perform all tests on the roundtripped step. -This creates a step which will fail up-to-date checks (it is equal only to itself), and will use the function you passed in to do the formatting pass. +## Implementing equality in terms of serialization -To create a step which can handle up-to-date checks properly, use the method ` FormatterStep create(String name, State state, Function stateToFormatter)`. Here's an example: +Spotless has infrastructure which uses the serialized form of your step to implement equality for you. Here is an example: ```java public final class ReplaceStep { @@ -63,10 +62,10 @@ public final class ReplaceStep { private static final class State implements Serializable { private static final long serialVersionUID = 1L; - private final CharSequence target; - private final CharSequence replacement; + private final String target; + private final String replacement; - State(CharSequence target, CharSequence replacement) { + State(String target, String replacement) { this.target = target; this.replacement = replacement; } @@ -83,8 +82,6 @@ The `FormatterStep` created above implements `equals` and `hashCode` based on th Oftentimes, a rule's state will be expensive to compute. `EclipseFormatterStep`, for example, depends on a formatting file. Ideally, we would like to only pay the cost of the I/O needed to load that file if we have to - we'd like to create the FormatterStep now but load its state lazily at the last possible moment. For this purpose, each of the `FormatterStep.create` methods has a lazy counterpart. Here are their signatures: ```java -FormatterStep createNeverUpToDate (String name, FormatterFunc function ) -FormatterStep createNeverUpToDateLazy(String name, Supplier functionSupplier) FormatterStep create (String name, State state , Function stateToFormatter) FormatterStep createLazy(String name, Supplier stateSupplier, Function stateToFormatter) ``` @@ -95,10 +92,37 @@ Here's a checklist for creating a new step for Spotless: - [ ] Class name ends in Step, `SomeNewStep`. - [ ] Class has a public static method named `create` that returns a `FormatterStep`. -- [ ] Has a test class named `SomeNewStepTest`. +- [ ] Has a test class named `SomeNewStepTest` that uses `StepHarness` or `StepHarnessWithFile` to test the step. - [ ] Test class has test methods to verify behavior. - [ ] Test class has a test method `equality()` which tests equality using `StepEqualityTester` (see existing methods for examples). +### Serialization roundtrip + +In order to support Gradle's configuration cache, all `FormatterStep` must be round-trip serializable. This is a bit tricky because step equality is based on the serialized form of the state, and `transient` can be used to take absolute paths out of the equality check. To make this work, roundtrip compatible steps can actually have *two* states: + +- `RoundtripState` which must be roundtrip serializable but has no equality constraints + - `FileSignature.Promised` for settings files and `JarState.Promised` for the classpath +- `EqualityState` which will never be reserialized and its serialized form is used for equality / hashCode checks + - `FileSignature` for settings files and `JarState` for the classpath + +```java +FormatterStep create(String name, + RoundtripState roundTrip, + SerializedFunction equalityFunc, + SerializedFunction formatterFunc) +FormatterStep createLazy(String name, + Supplier roundTrip, + SerializedFunction equalityFunc, + SerializedFunction formatterFunc) +``` + +### Third-party dependencies via reflection or compile-only source sets + +Most formatters are going to use some kind of third-party jar. Spotless integrates with many formatters, some of which have incompatible transitive dependencies. To address this, we resolve third-party dependencies using [`JarState`](https://github.com/diffplug/spotless/blob/b26f0972b185995d7c6a7aefa726c146d24d9a82/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java#L118). To call methods on the classes in that `JarState`, you can either use reflection or a compile-only source set. See [#524](https://github.com/diffplug/spotless/issues/524) for examples of both approaches. + +- Adding a compile-only sourceset is easier to read and probably a better approach for most cases. +- Reflection is more flexible, and might be a better approach for a very simple API. + ### Accessing the underlying File In order for Spotless' model to work, each step needs to look only at the `String` input, otherwise they cannot compose. However, there are some cases where the source `File` is useful, such as to look at the file extension. In this case, you can pass a `FormatterFunc.NeedsFile` instead of a `FormatterFunc`. This should only be used in [rare circumstances](https://github.com/diffplug/spotless/pull/637), be careful that you don't accidentally depend on the bytes inside of the `File`! @@ -112,23 +136,14 @@ There are many great formatters (prettier, clang-format, black, etc.) which live Because of Spotless' up-to-date checking and [git ratcheting](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet), Spotless actually doesn't have to call formatters very often, so even an expensive shell call for every single invocation isn't that bad. Anything that works is better than nothing, and we can always speed things up later if it feels too slow (but it probably won't). -## How to enable the _ext projects - -The `_ext` projects are disabled per default, since: +## Lints -* some of the projects perform vast downloads at configuration time -* the downloaded content may change on server side and break CI builds +Spotless is primarily a formatter, not a linter. But, if something goes wrong during formatting, it's better to model that as a lint with line numbers rather than just a naked exception. There are two ways to go about this: +- at any point during the formatting process, you can throw a `Lint.atLine(int line, ...)` exception. This will be caught and turned into a lint. +- or you can override the `default List lint(String content, File file)` method. This method will only run if the step did not already throw an exception. -The `_ext` can be activated via the root project property `com.diffplug.spotless.include.ext`. - -Activate the the property via command line, like for example: - -``` -gradlew -Pcom.diffplug.spotless.include.ext=true build -``` - -Or set the property in your user `gradle.properties` file, which is especially recommended if you like to work with the `_ext` projects using IDEs. +Don't go lint crazy! By default, all lints are build failures. Users have to suppress them explicitly if they want to continue. ## How to add a new plugin for a build system @@ -144,6 +159,37 @@ The gist of it is that you will have to: If you get something running, we'd love to host your plugin within this repo as a peer to `plugin-gradle` and `plugin-maven`. +## Run tests + +To run all tests, simply do + +> gradlew test + +Since that takes some time, you might only want to run the tests +concerning what you are working on: + +```shell +# Run only from test from the "lib" project +./gradlew :testlib:test --tests com.diffplug.spotless.generic.IndentStepTest + +# Run only one test from the "plugin-maven" project +./gradlew :plugin-maven:test --tests com.diffplug.spotless.maven.pom.SortPomMavenTest + +# Run only one test from the "plugin-gradle" project +./gradlew :plugin-gradle:test --tests com.diffplug.gradle.spotless.FreshMarkExtensionTest +``` + +## Check and format code + +Before creating a pull request, you might want to format (yes, spotless is formatted by spotless) +the code and check for possible bugs + +* `./gradlew spotlessApply` +* `./gradlew spotbugsMain` + +These checks are also run by the automated pipeline when you submit a pull request, if +the pipeline fails, first check if the code is formatted and no bugs were found. + ## Integration testing ### Gradle - locally @@ -200,7 +246,7 @@ If it doesn't work, you can check the JitPack log at `https://jitpack.io/com/git ### Maven -Run `./gradlew publishToMavenLocal` to publish this to your local repository. The maven plugin is not published to JitPack due to [jitpack/jitpack.io#4112](https://github.com/jitpack/jitpack.io/issues/4112). +Run `./gradlew publishToMavenLocal` to publish this to your local repository. You can also use the JitPack artifacts, using the same principles as Gradle above. ## License diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 0ae5167a13..76d0ce19ab 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ If you are submitting a **bug**, please include the following: - [ ] summary of problem -- [ ] gradle or maven version +- [ ] Gradle or Maven version - [ ] spotless version - [ ] operating system and version - [ ] copy-paste your full Spotless configuration block(s), and a link to a public git repo that reproduces the problem if possible diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index bb9ef60188..406a0c1e66 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,8 @@ Please **DO NOT FORCE PUSH**. Don't worry about messy history, it's easier to do code review if we can tell what happened after the review, and force pushing breaks that. -Please make sure that your [PR allows edits from maintainers](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork/). Sometimes its faster for us to just fix something than it is to describe how to fix it. +Please make sure that your [PR allows edits from maintainers](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork). Sometimes it's faster for us to just fix something than it is to describe how to fix it. -![Allow edits from maintainers](https://help.github.com/assets/images/help/pull_requests/allow-maintainers-to-make-edits-sidebar-checkbox.png) +Allow edits from maintainers After creating the PR, please add a commit that adds a bullet-point under the `[Unreleased]` section of [CHANGES.md](https://github.com/diffplug/spotless/blob/main/CHANGES.md), [plugin-gradle/CHANGES.md](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md), and [plugin-maven/CHANGES.md](https://github.com/diffplug/spotless/blob/main/plugin-maven/CHANGES.md) which includes: diff --git a/README.md b/README.md index 6ace09631d..a40b9edb0d 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,45 @@ # Spotless: Keep your code spotless - -[![Circle CI](https://circleci.com/gh/diffplug/spotless/tree/main.svg?style=shield)](https://circleci.com/gh/diffplug/spotless/tree/main) -[![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless) -[![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) - +[![Gradle Plugin](https://img.shields.io/gradle-plugin-portal/v/com.diffplug.spotless?color=blue&label=gradle%20plugin)](plugin-gradle) +[![Maven Plugin](https://img.shields.io/maven-central/v/com.diffplug.spotless/spotless-maven-plugin?color=blue&label=maven%20plugin)](plugin-maven) +[![SBT Plugin](https://img.shields.io/badge/sbt%20plugin-0.1.3-blue)](https://github.com/moznion/sbt-spotless) -Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | sql | typeScript | vue | yaml | anything> using <gradle | maven | anything>. +Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | shell | sql | typeScript | vue | yaml | anything> using <gradle | maven | sbt | anything>. 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)) + +```console +user@machine repo % ./gradlew build +:spotlessJavaCheck FAILED + The following files had format violations: + src\main\java\com\diffplug\gradle\spotless\FormatExtension.java + -\t\t¡¡¡¡if¡(targets.length¡==¡0)¡{ + +\t\tif¡(targets.length¡==¡0)¡{ + Run './gradlew spotlessApply' to fix these violations. +user@machine repo % ./gradlew spotlessApply +:spotlessApply +BUILD SUCCESSFUL +user@machine repo % ./gradlew build +BUILD SUCCESSFUL +``` + ## [â‡ī¸ Spotless for Maven](plugin-maven) + +```console +user@machine repo % mvn spotless:check +[ERROR] > The following files had format violations: +[ERROR] src\main\java\com\diffplug\gradle\spotless\FormatExtension.java +[ERROR] -\t\t¡¡¡¡if¡(targets.length¡==¡0)¡{ +[ERROR] +\t\tif¡(targets.length¡==¡0)¡{ +[ERROR] Run 'mvn spotless:apply' to fix these violations. +user@machine repo % mvn spotless:apply +[INFO] BUILD SUCCESS +user@machine repo % mvn spotless:check +[INFO] BUILD SUCCESS +``` + ## [â‡ī¸ Spotless for SBT (external for now)](https://github.com/moznion/sbt-spotless) ## [Other build systems](CONTRIBUTING.md#how-to-add-a-new-plugin-for-a-build-system) @@ -30,8 +52,8 @@ It's easy to build such a function, but there are some gotchas and lots of integ ## Current feature matrix @@ -94,26 +130,40 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`generic.ReplaceStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`generic.TrimTrailingWhitespaceStep`](lib/src/main/java/com/diffplug/spotless/generic/TrimTrailingWhitespaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`antlr4.Antlr4FormatterStep`](lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`biome.BiomeStep`](lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`cpp.ClangFormatStep`](lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`cpp.EclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | +| [`go.GofmtFormatStep`](lib/src/main/java/com/diffplug/spotless/go/GofmtFormatStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | +| [`gherkin.GherkinUtilsStep`](lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`groovy.GrEclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`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.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :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: | +| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`json.JacksonJsonStep`](lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`json.JsonPatchStep`](lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | -| [`markdown.FlexmarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | +| [`markdown.FlexmarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FlexmarkStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`npm.EslintFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | -| [`pom.SortPomStepStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStepStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | +| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`protobuf.BufStep`](lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | +| [`rdf.RdfFormatterStep`](lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: | +| [`shell.ShfmtStep`](lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`yaml.JacksonYamlStep`](lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [(Your FormatterStep here)](CONTRIBUTING.md#how-to-add-a-new-formatterstep) | :white_large_square: | :white_large_square: | :white_large_square: | :white_large_square: | @@ -125,14 +175,14 @@ Once someone has filled in one square of the formatter/build system matrix, it's ## Acknowledgements -- Thanks to [Konstantin Lutovich](https://github.com/lutovich) for [implementing and maintaining the maven plugin](https://github.com/diffplug/spotless/pull/188), as well as fixing [remote-build cache support for Gradle](https://github.com/diffplug/spotless/pull/571). +- Thanks to [Konstantin Lutovich](https://github.com/lutovich) for [implementing and maintaining the Maven plugin](https://github.com/diffplug/spotless/pull/188), as well as fixing [remote-build cache support for Gradle](https://github.com/diffplug/spotless/pull/571). - Thanks to [Frank Vennemeyer](https://github.com/fvgh) for [Groovy support via greclipse](https://github.com/diffplug/spotless/issues/13), [C++ support via CDT](https://github.com/diffplug/spotless/issues/232), [XML support via WTP](https://github.com/diffplug/spotless/pull/241) and a huge body of work with other eclipse-based formatters. - Thanks to [Jonathan Bluett-Duncan](https://github.com/jbduncan) for - implementing up-to-date checking [#31](https://github.com/diffplug/spotless/issues/31) - breaking spotless into libraries [#56](https://github.com/diffplug/spotless/issues/56) - lots of other things, but especially the diff support in `spotlessCheck` - constant improvements on a variety of topics with high-quality code reviews -- Thanks to [Daz DeBoer](https://github.com/bigdaz) for the reworking the guts of our gradle plugin to support [buildcache](https://github.com/diffplug/spotless/pull/576), [InputChanges](https://github.com/diffplug/spotless/pull/607), and [lazy configuration](https://github.com/diffplug/spotless/pull/617). +- Thanks to [Daz DeBoer](https://github.com/bigdaz) for the reworking the guts of our Gradle plugin to support [buildcache](https://github.com/diffplug/spotless/pull/576), [InputChanges](https://github.com/diffplug/spotless/pull/607), and [lazy configuration](https://github.com/diffplug/spotless/pull/617). - Thanks to [Richard Willis](https://github.com/badsyntax) for creating the [VS Code extension for Spotless Gradle](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle). - Thanks to [Ryan Gurney](https://github.com/ragurney) for creating the [IntelliJ plugin for Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle). - Thanks to [Markus Heberling](https://github.com/tisoft) for adding [generic native formatters](https://github.com/diffplug/spotless/pull/949), [jsr-223 formatters](https://github.com/diffplug/spotless/pull/945), and [maven pom sorting](https://github.com/diffplug/spotless/pull/946). @@ -151,7 +201,7 @@ Once someone has filled in one square of the formatter/build system matrix, it's - Thanks to [Baptiste Mesta](https://github.com/baptistemesta) for - porting the DBeaver formatter to Spotless, and thanks to [DBeaver](https://dbeaver.jkiss.org/) and [its authors](https://github.com/serge-rider/dbeaver/graphs/contributors) for their excellent SQL formatter. - making license headers date-aware [#179](https://github.com/diffplug/spotless/pull/179) -- Thanks to [vmdominguez](https://github.com/vmdominguez) and [Luis Fors](https://github.com/luis-fors-cb) for adding the ability to limit formatting to specific files in gradle ([#322](https://github.com/diffplug/spotless/pull/322)) and maven ([#392](https://github.com/diffplug/spotless/pull/392)), respectively. +- Thanks to [vmdominguez](https://github.com/vmdominguez) and [Luis Fors](https://github.com/luis-fors-cb) for adding the ability to limit formatting to specific files in Gradle ([#322](https://github.com/diffplug/spotless/pull/322)) and Maven ([#392](https://github.com/diffplug/spotless/pull/392)), respectively. - Thanks to [bender316](https://github.com/bender316) for fixing classloading on Java 9 ([#426](https://github.com/diffplug/spotless/pull/426)). - Thanks to [Stefan Oehme](https://github.com/oehme) for tons of help on the internal mechanics of Gradle. - Thanks to [eyalkaspi](https://github.com/eyalkaspi) for adding configurable date ranges to the date-aware license headers. @@ -159,7 +209,7 @@ Once someone has filled in one square of the formatter/build system matrix, it's - Thanks to [Oliver Horn](https://github.com/ohorn) for adding AOSP support for Spotless' google-java-format integration. - Formatting by Eclipse - Special thanks to [Mateusz Matela](https://waynebeaton.wordpress.com/2015/03/15/great-fixes-for-mars-winners-part-i/) for huge improvements to the eclipse code formatter! -- Thanks to [Zac Sweers](https://github.com/ZacSweers) for fixing the highly requested ktlint 0.34+ support ([#469](https://github.com/diffplug/spotless/pull/469)), multiple build updates and fixing a gradle deprecation warning ([#434](https://github.com/diffplug/spotless/pull/434) and others). +- Thanks to [Zac Sweers](https://github.com/ZacSweers) for fixing the highly requested ktlint 0.34+ support ([#469](https://github.com/diffplug/spotless/pull/469)), multiple build updates and fixing a Gradle deprecation warning ([#434](https://github.com/diffplug/spotless/pull/434) and others). - Thanks to [Stephen Panaro](https://github.com/smpanaro) for adding support for ktlint FilenameRule ([#974](https://github.com/diffplug/spotless/pull/974)). - Thanks to [Nelson Osacky](https://github.com/runningcode) for android doc improvements, versions bump, and a build improvement. - Thanks to [Stanley Shyiko](https://github.com/shyiko) for his help integrating [ktlint](https://github.com/shyiko/ktlint). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..56294d0a51 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +For security disclosures, please send an email to spotless-security@diffplug.com diff --git a/_ext/eclipse-base/CHANGES.md b/_ext/eclipse-base/CHANGES.md deleted file mode 100644 index 796ed584fd..0000000000 --- a/_ext/eclipse-base/CHANGES.md +++ /dev/null @@ -1,53 +0,0 @@ -# spotless-eclipse-base - -We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.2.1`). - -## [Unreleased] - -## [3.5.2] - 2021-10-23 -### Fixed -* Racing condition when cleaning up temporary workspace on JVM runtime shutdown (see [#967](https://github.com/diffplug/spotless/issues/967)). Can lead to error logs and remaining files in workspace. - -## [3.5.1] - 2021-10-16 -### Fixed -* ~~Racing condition when cleaning up temporary workspace on JVM runtime shutdown (see [#967](https://github.com/diffplug/spotless/issues/967)). Can lead to error logs and remaining files in workspace.~~ - -## [3.5.0] - 2021-06-20 -### Added -* Support of `org.eclipse.core.resources` version `3.15.0` required by Eclipse `4.20`. -* Minimum required Java version changed from 8 to 11. - -## [3.4.2] - 2020-12-26 -### Fixed -* `org.eclipse.osgi` version `3.16.100` does not allow `null` as Debug service for `CloseableBundleFile`. - -## [3.4.1] - 2020-09-24 -### Fixed -* Restored scope of transitive dependencies to 'compile'. - -## [3.4.0] - 2020-09-23 -### Added -* Upgraded to `org.eclipse.osgi` version `3.16`. - -## [3.3.0] - 2020-03-04 -### Added -* Added support of plugin extensions without activator ([#533](https://github.com/diffplug/spotless/issues/533)). Required since `org.eclipse.core.filesystem` version `1.7.600` (see -[Bug 550548](https://bugs.eclipse.org/bugs/show_bug.cgi?id=550548)) -* Updated configuration via single interface implementation. Functional based configuration still supported. - -## [3.2.1] - 2019-09-03 -* Fixed deletion of temporary workspace. ([#447](https://github.com/diffplug/spotless/issues/447)) - -## [3.2.0] - 2019-06-30 -* Added support of Eclipse 4.12 framework wiring. ([#413](https://github.com/diffplug/spotless/issues/413)) - -## [3.1.1] - 2019-06-04 -* Fixed problem handling URL escaped characters in JAR file location. ([#401](https://github.com/diffplug/spotless/issues/401)) - -## [3.1.0] - 2019-02-10 -* Added logging service based on SLF4J. ([#236](https://github.com/diffplug/spotless/issues/236)) -* Updated internal interfaces to support `org.eclipse.osgi` version 3.13. -* Corrected documentation of version number usage in `gradle.properties`. - -## [3.0.0] - 2018-06-18 -* Initial release! diff --git a/_ext/eclipse-base/LICENSE.txt b/_ext/eclipse-base/LICENSE.txt deleted file mode 100644 index 3d967aee74..0000000000 --- a/_ext/eclipse-base/LICENSE.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. \ No newline at end of file diff --git a/_ext/eclipse-base/README.md b/_ext/eclipse-base/README.md deleted file mode 100644 index 39a03591ab..0000000000 --- a/_ext/eclipse-base/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# spotless-eclipse-base - -Eclipse formatters are embedded in plugins serving multiple purposes and not necessarily supporting headless builds. Hence the plugin interfaces are depending on various Eclipse plugins and services not required for the formatting purpose. - -Spotless provides its own plugin framework with `com.diffplug.spotless.JarState`. This allows Spotless to use different Eclipse versions in parallel. - - -The `com.diffplug.gradle.spotless:spotless-eclipse-base` artifact mocks the redundant Eclipse OSGI/plugin framework for Spotless. Furthermore it provides Eclipse services adapted for Spotless's own use, which avoids for example the creation of a permanent Eclipse workspace and reduces dependencies on Eclipse modules unused by the Eclipse code formatters. - -## Usage - -Include the artifact in your Spotless Eclipse formatter project, where the major version must match the Eclipse core version your formatter supports. The exact default version should be selected by the **lib-extra**. -Minor versions indicate a change in the minimum Eclipse (core/resource) dependencies. -Patch versions are always backward compatible. - - -```Gradle -dependencies { - compile "com.diffplug.spotless:spotless-eclipse-base:3.+" -} -``` - -In the constructor of your formatter, the Spotless Eclipse Framework can be configured depending on your formatter's requirements. For example the JDT formatter can be configured like: - -```Java -public EclipseFormatterStepImpl(Properties settings) throws Exception { - SpotlessEclipseFramework.setup(plugins -> { - plugins.applyDefault(); - plugins.add(new org.eclipse.jdt.core.JavaCore()); - }); - ... -``` - -The framework also supports fat JARs, providing multiple plugins. -In this cases the resources required by plugins, especially the `META-INF` and plugin information, must be located in locations unique to the plugin. -For this purpose the framework expects that these resources are stored in a sub-directory -which has the name of the package containing the plugin. For example in case the JDT plugin -is included in your formatter fat JAR, the directory structure should be: - -``` -+ resources -| -+--+ org.eclipse.jdt.core -| | -| +--+ META-INF -| | | -| | +-- MANIFEST.MF -| | -| +--- plugin.properties -| | -| +--- plugin.xml - -``` - -## Build - -To publish a new version, update the `_ext/eclipse-base/gradle.properties` appropriately and see [CONTRIBUTING.md](../../CONTRIBUTING.md) how to enable -`_ext` projects. - -## License - -Spotless at large is under the Apache 2.0 license, but this jar is under the EPL v1. diff --git a/_ext/eclipse-base/build.gradle b/_ext/eclipse-base/build.gradle deleted file mode 100644 index 88255b57b3..0000000000 --- a/_ext/eclipse-base/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -apply from: rootProject.file('_ext/gradle/java-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -ext { - developers = [ - fvgh: [ name: 'Frank Vennemeyer', email: 'frankgh@zoho.com' ] - ] -} - -dependencies { - api("org.eclipse.platform:org.eclipse.core.resources:${VER_ECLIPSE_CORE_RESOURCES}") { - exclude group: 'org.eclipse.platform', module: 'org.eclipse.ant.core' - exclude group: 'org.eclipse.platform', module: 'org.eclipse.core.expressions' - } - api("org.slf4j:slf4j-api:${VER_SLF4J}") - - testImplementation("org.slf4j:slf4j-simple:${VER_SLF4J}") -} - -jar { - manifest { - from 'src/main/resources/META-INF/MANIFEST.MF' - } -} - -////////// -// Test // -////////// -sourceSets { - // Use JAR file with all resources for Eclipse-Base integration-tests - test.runtimeClasspath = jar.outputs.files + sourceSets.test.output + sourceSets.test.compileClasspath -} diff --git a/_ext/eclipse-base/gradle.properties b/_ext/eclipse-base/gradle.properties deleted file mode 100644 index 9f8774a417..0000000000 --- a/_ext/eclipse-base/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ -artifactId=spotless-eclipse-base -description=Eclipse bundle controller and services for Spotless - -# Build requirements -VER_JAVA=11 - -# Compile dependencies -VER_ECLIPSE_CORE_RESOURCES=[3.15.0,4.0.0[ diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseConfig.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseConfig.java deleted file mode 100644 index c8d1fb4db3..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base; - -/** - * Obtain configuration for Eclipse framework setup - *

- * The setup consitst of 4 phases: - *

    - *
  1. Registration of framework bundles
  2. - *
  3. Registration of servicves
  4. - *
  5. Resitration of extension points, activation of bundles/plugins
  6. - *
  7. Customize bundle/plugin configuration
  8. - *
- */ -public interface SpotlessEclipseConfig { - default public void registerBundles(SpotlessEclipseCoreConfig config) { - config.applyDefault(); - } - - default public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - } - - default public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - } - - default public void customize() {} -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseCoreConfig.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseCoreConfig.java deleted file mode 100644 index 4f3aa9c2a2..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseCoreConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base; - -import org.osgi.framework.BundleActivator; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.DefaultBundles; -import com.diffplug.spotless.extra.eclipse.base.osgi.BundleConfig; - -/** - * Configuration of core bundles which shall be provided by the - * {@link SpotlessEclipseFramework}. - *

- * Core bundles are not accessed via the plugin registry, but by static methods. - * Hence they do not require a registration, which allows a lightweight - * setup. - *

- * See {@code org.eclipse.core.internal.runtime.PlatformActivator} implementation for details. - */ -public class SpotlessEclipseCoreConfig extends BundleConfig { - - /** - * Don't instantiate and call {@link SpotlessEclipseConfig} directly. - * Registered bundles should only be instantiated once, since - * older bundles still abusing singletons for access. - */ - SpotlessEclipseCoreConfig() {} - - @Override - public void applyDefault() { - add(SpotlessEclipseFramework.DefaultBundles.createAll()); - } - - @Override - protected BundleActivator create(DefaultBundles bundle) { - return bundle.create(); - } - - @Override - protected int getDefaultState(DefaultBundles bundle) { - return bundle.getDesiredState(); - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java deleted file mode 100644 index 44be5f6805..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; - -import javax.xml.parsers.SAXParserFactory; - -import org.eclipse.core.internal.runtime.InternalPlatform; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; - -import com.diffplug.spotless.extra.eclipse.base.osgi.BundleConfig; -import com.diffplug.spotless.extra.eclipse.base.osgi.BundleController; -import com.diffplug.spotless.extra.eclipse.base.runtime.PluginRegistrar; - -/** Setup a framework for Spotless Eclipse based formatters */ -public final class SpotlessEclipseFramework { - /** Spotless demands for internal formatter chains Unix (LF) line endings. */ - public static final String LINE_DELIMITER = "\n"; - - /** - * Default core bundles required by most plugins. - */ - public enum DefaultBundles { - /** - * Plugins ask the platform whether core runtime bundle is in debug mode. - *

- * Note that the bundle requires the - * {@link org.eclipse.osgi.service.environment.EnvironmentInfo} - * service. - *

- *

- * Per default, the platform is not activated. Some plugins use this information - * to determine whether they are running in a headless modes (without IDE). - *

- */ - PLATFORM(org.eclipse.core.internal.runtime.PlatformActivator.class, Bundle.RESOLVED), - /** The Spotless {@link BundleController} wraps the OSGi layer. But the plugin/extension look-up still uses the Eclipse OSGi registry.*/ - REGISTRY(org.eclipse.core.internal.registry.osgi.Activator.class), - /** Eclipse preferences always check whether this bundle has been activated before preference are set.*/ - PREFERENCES(org.eclipse.core.internal.preferences.Activator.class), - /** The common runtime provides provides common services, like log and service adapters registry. */ - COMMON(org.eclipse.core.internal.runtime.Activator.class); - - private final Class activatorClass; - private final int state; - - private DefaultBundles(Class clazz) { - this(clazz, Bundle.ACTIVE); - } - - private DefaultBundles(Class clazz, int state) { - activatorClass = clazz; - this.state = state; - } - - /** Create new bundle activator instance. */ - public BundleActivator create() { - return createInstance(activatorClass); - } - - public int getDesiredState() { - return state; - } - - /** Create bundle activator instances for all enumerated values. */ - public static List createAll() { - return Arrays.stream(values()) - .map(value -> new BundleConfig.Entry(value.create(), value.getDesiredState())).collect(Collectors.toList()); - } - } - - /** - * Default plugins required by most Spotless formatters. - *

- * Eclipse plugins are OSGI bundles themselves and do not necessarily derive from the Eclipse Plugin class. - * {@link BundleActivator} implementation may as well serve as plugins. - * All plugins must provide a MANIFEST.MF, plugin.properties and plugin.xml description file, - * required for the plugin registration. - *

- */ - public enum DefaultPlugins { - /** - * The resources plugin initialized the Eclipse workspace and allows URL look-up. - * Most formatters using the workspace to resolve URLs or create - * file interfaces. - * See {@code org.eclipse.core.resources.IFile} implementation for details. - */ - RESOURCES(org.eclipse.core.resources.ResourcesPlugin.class); - - private final Class pluginClass; - - DefaultPlugins(Class clazz) { - pluginClass = clazz; - } - - /** Create new plugin instance. */ - public BundleActivator create() { - return createInstance(pluginClass); - } - - /** Create plugin instances for all enumerated values. */ - public static List createAll() { - return Arrays.stream(values()).map(value -> value.create()).collect(Collectors.toList()); - } - } - - private static T createInstance(Class clazz) { - try { - Constructor ctor = clazz.getConstructor(); - return ctor.newInstance(new Object[]{}); - } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { - throw new RuntimeException("Failed to create instance for: " + clazz.getCanonicalName(), e); - } - } - - private static SpotlessEclipseFramework INSTANCE = null; - - /** - * Creates and configures a new {@link SpotlessEclipseFramework} using - * {@link DefaultBundles}, {@link DefaultPlugins} and default {@link SpotlessEclipseServiceConfig}. - * If there is already a an instance, the call is ignored. - * @return False if the {@link SpotlessEclipseFramework} instance already exists, true otherwise. - * @throws BundleException Throws exception in case the setup failed. - * @deprecated Use {@link #setup(SpotlessEclipseConfig)} instead. - */ - @Deprecated - public synchronized static boolean setup() throws BundleException { - return setup(plugins -> plugins.applyDefault()); - } - - /** - * Creates and configures a new {@link SpotlessEclipseFramework} using - * {@link DefaultBundles} and {@link DefaultPlugins}. - * If there is already a an instance, the call is ignored. - * @param plugins Eclipse plugins (which are also OSGi bundles) to start - * @return False if the {@link SpotlessEclipseFramework} instance already exists, true otherwise. - * @throws BundleException Throws exception in case the setup failed. - * @deprecated Use {@link #setup(SpotlessEclipseConfig)} instead. - */ - @Deprecated - public synchronized static boolean setup(Consumer plugins) throws BundleException { - return setup(config -> config.applyDefault(), plugins); - } - - /** - * Creates and configures a new {@link SpotlessEclipseFramework} using {@link DefaultBundles}. - * If there is already a an instance, the call is ignored. - * @param config Framework service configuration - * @param plugins Eclipse plugins (which are also OSGi bundles) to start - * @return False if the {@link SpotlessEclipseFramework} instance already exists, true otherwise. - * @throws BundleException Throws exception in case the setup failed. - * @deprecated Use {@link #setup(SpotlessEclipseConfig)} instead. - */ - @Deprecated - public synchronized static boolean setup(Consumer config, Consumer plugins) throws BundleException { - return setup(core -> core.applyDefault(), config, plugins); - } - - /** - * Creates and configures a new {@link SpotlessEclipseFramework} if there is none. - * If there is already a an instance, the call is ignored. - * @param core Activators of core bundles - * @param services Framework service configuration - * @param plugins Eclipse plugins to start - * @return False if the {@link SpotlessEclipseFramework} instance already exists, true otherwise. - * @throws BundleException Throws exception in case the setup failed. - * @deprecated Use {@link #setup(SpotlessEclipseConfig)} instead. - */ - @Deprecated - public synchronized static boolean setup(Consumer core, Consumer services, Consumer plugins) throws BundleException { - if (null != INSTANCE) { - return false; - } - setup(new SpotlessEclipseConfig() { - @Override - public void registerBundles(SpotlessEclipseCoreConfig config) { - core.accept(config); - } - - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - services.accept(config); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - plugins.accept(config); - } - }); - return true; - } - - /** - * Creates and configures a new {@link SpotlessEclipseFramework} if there is none. - * If there is already a an instance, the call is ignored. - * @param config Configuration - * @throws BundleException Throws exception in case the setup failed. - */ - public synchronized static void setup(SpotlessEclipseConfig config) throws BundleException { - if (null == INSTANCE) { - INSTANCE = new SpotlessEclipseFramework(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - INSTANCE.shutdown(); - } catch (Exception e) { - //At shutdown everything is just done on best-efforts basis - } - })); - - config.registerServices(INSTANCE.getServiceConfig()); - - SpotlessEclipseCoreConfig coreConfig = new SpotlessEclipseCoreConfig(); - config.registerBundles(coreConfig); - INSTANCE.startCoreBundles(coreConfig); - - SpotlessEclipsePluginConfig pluginConfig = new SpotlessEclipsePluginConfig(); - config.activatePlugins(pluginConfig); - for (Class extension : pluginConfig.getExtensions()) { - INSTANCE.addExtension(extension); - } - for (BundleConfig.Entry plugin : pluginConfig.get()) { - INSTANCE.addPlugin(plugin.state, plugin.activator); - } - - config.customize(); - } - } - - private final Function registry; - private final BundleController controller; - - private SpotlessEclipseFramework() throws BundleException { - - controller = new BundleController(); - registry = (pluginBundle) -> { - return PluginRegistrar.register(pluginBundle); - }; - } - - void shutdown() { - controller.shutdown(); - } - - private SpotlessEclipseServiceConfig getServiceConfig() { - return controller.getServices(); - } - - private void addExtension(Class clazzInBundleJar) throws BundleException { - controller.addBundle(clazzInBundleJar, registry); - } - - private void addPlugin(int state, BundleActivator plugin) throws BundleException { - controller.addBundle(state, plugin, registry); - } - - private void startCoreBundles(SpotlessEclipseCoreConfig coreConfig) throws BundleException { - //The SAXParserFactory.class is required for parsing the plugin XML files - addMandatoryServiceIfMissing(SAXParserFactory.class, SAXParserFactory.newInstance()); - - /* - * Since org.eclipse.core.runtime version 3.15.300, the Eclipse bundle look-up is accomplished - * via the wiring framework, which requires starting the InternalPlatform. - * The internal platform initialization is customized by the services - * registered to the controller. - */ - InternalPlatform.getDefault().start(controller); - - for (BundleConfig.Entry coreBundle : coreConfig.get()) { - try { - BundleContext context = controller.createContext(coreBundle.state); - coreBundle.activator.start(context); - } catch (Exception e) { - throw new BundleException(String.format("Failed to start %s", coreBundle.activator.getClass().getName()), BundleException.ACTIVATOR_ERROR, e); - } - } - } - - private void addMandatoryServiceIfMissing(Class interfaceClass, S service) { - if (null == controller.getServiceReference(interfaceClass)) { - controller.getServices().add(interfaceClass, service); - } - } -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipsePluginConfig.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipsePluginConfig.java deleted file mode 100644 index 2569c12cd9..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipsePluginConfig.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.DefaultPlugins; -import com.diffplug.spotless.extra.eclipse.base.osgi.BundleConfig; - -/** - * Configuration of plugins which shall be provided by the - * {@link SpotlessEclipseFramework}. - *

- * Plugins are registered via the Eclipse registry. Therefore they - * required a {@code plugin.xml} configuration description. - * Note that a plugin does not necessarily derive from the - * Eclipse {@link org.eclipse.core.runtime.Plugin} class. - *

- *

- * Some plugins are pure extensions without any activator. - * For resource and plugin information lookup, a class can be - * specified which is in the JAR containing the resources and - * plugin information. This lookup procedure also supports - * fat JAR lookups as described in the ReadMe.md. - *

- * @see org.eclipse.core.runtime.RegistryFactory - */ -public class SpotlessEclipsePluginConfig extends BundleConfig { - private final List> extensions; - - /** - * Don't instantiate and call {@link SpotlessEclipseConfig} directly. - * Registered plugins and extensions should only be instantiated once, since - * some still abusing singletons for access. - */ - SpotlessEclipsePluginConfig() { - extensions = new ArrayList<>(); - } - - /** Add an extension plugin, identified by a class of the plugin. */ - public void add(Class extensionClass) { - Objects.requireNonNull(extensionClass, "Plugin extension class must nor be null"); - extensions.add(extensionClass); - } - - /** Add a set of default bundles with their default states */ - public void add(Class... extensionClasses) { - Arrays.asList(extensionClasses).forEach(extensionClass -> add(extensionClass)); - } - - /** Returns the current configuration */ - public List> getExtensions() { - return extensions; - } - - @Override - public void applyDefault() { - add(SpotlessEclipseFramework.DefaultPlugins.createAll()); - } - - @Override - protected BundleActivator create(DefaultPlugins bundle) { - return bundle.create(); - } - - @Override - protected int getDefaultState(DefaultPlugins bundle) { - return Bundle.ACTIVE; - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseServiceConfig.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseServiceConfig.java deleted file mode 100644 index f0b9a97cc4..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseServiceConfig.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; - -import java.util.Map; -import java.util.function.BiFunction; - -import org.eclipse.core.runtime.content.IContentTypeManager; -import org.eclipse.core.runtime.preferences.IPreferencesService; -import org.eclipse.equinox.log.ExtendedLogReaderService; -import org.eclipse.equinox.log.ExtendedLogService; -import org.eclipse.osgi.service.datalocation.Location; -import org.eclipse.osgi.service.debug.DebugOptions; -import org.eclipse.osgi.service.environment.EnvironmentInfo; -import org.osgi.framework.ServiceException; -import org.osgi.service.log.LogLevel; - -import com.diffplug.spotless.extra.eclipse.base.service.*; - -/** - * Configuration/Provision of services which shall be provided by the {@link SpotlessEclipseFramework}. - *

- * The services provide basic functions/configuration to the {@link SpotlessEclipseFramework} bundles use the services. - * Hence the services can be used to customize the behavior of core bundles and plugins configured - * in the {@link SpotlessEclipseCoreConfig} and {@link SpotlessEclipsePluginConfig}. - *

- */ -public interface SpotlessEclipseServiceConfig { - - /** Sets property/preference value available to all bundles, plugins and services. */ - void set(String key, String value); - - /** Sets property/preference values available to all bundles, plugins and services. */ - default void set(Map properties) { - properties.forEach((k, v) -> this.set(k, v)); - } - - /** - * Add custom service to collection. - *

- * Only one service per interface is allowed. - * A service instance implementing multiple interfaces, can be added for each interface. - *

- * Please refer to the default method implementation for examples. - * - * @param interfaceClass Service interface - * @param service Service instance - * @throws ServiceException in case service has already been configured - */ - public void add(Class interfaceClass, S service) throws ServiceException; - - /** - * Spotless formatters should not be configured by environment variables, and - * they shall be OS independent. - * @throws ServiceException in case service has already been configured - */ - default public void hideEnvironment() throws ServiceException { - add(EnvironmentInfo.class, new HiddenEnvironment()); - } - - /** - * Eclipse provides means to lookup the file content type, e.g. by file name extensions. - * This possibility is not required by most Spotless formatters. - * @throws ServiceException in case service has already been configured - */ - default public void ignoreContentType() throws ServiceException { - add(IContentTypeManager.class, new NoContentTypeSpecificHandling()); - } - - /** - * Disable Eclipse internal debugging. - * @throws ServiceException in case service has already been configured - */ - default public void disableDebugging() throws ServiceException { - add(DebugOptions.class, new NoDebugging()); - } - - /** - * Ignore accesses of unsupported preference. - * @throws ServiceException in case service has already been configured - */ - default public void ignoreUnsupportedPreferences() throws ServiceException { - add(IPreferencesService.class, new NoEclipsePreferences()); - } - - /** - * Use temporary locations in case plugins require to store file. - * These files (for example workspace preferences) will be deleted - * as soon as the application terminates. - * @throws ServiceException in case service has already been configured - */ - default public void useTemporaryLocations() throws ServiceException { - add(Location.class, new TemporaryLocation()); - } - - /** - * In case the string which will be formatted does not contain any - * line delimiter (single line), Eclipse falls back to use the - * system property. - * Change the system default to the UNIX line separator as required - * by Spotless. - * @throws ServiceException in case service has already been configured - */ - default public void changeSystemLineSeparator() throws ServiceException { - System.setProperty("line.separator", LINE_DELIMITER); - } - - /** - * Adds Eclipse logging service Slf4J binding. - * @throws ServiceException in case service has already been configured - */ - default public void useSlf4J(String loggerName) { - useSlf4J(loggerName, (s, l) -> s); - } - - /** - * Adds Eclipse logging service Slf4J binding with a message customizer function. - * @throws ServiceException in case service has already been configured - */ - default public void useSlf4J(String loggerName, BiFunction messageCustomizer) throws ServiceException { - SingleSlf4JService slf4jServce = new SingleSlf4JService(loggerName, messageCustomizer); - add(ExtendedLogService.class, slf4jServce); - add(ExtendedLogReaderService.class, slf4jServce); - slf4jServce.debug("Initialized Eclipse logging service."); - } - - /** - * Applies the default configurations. - * @throws ServiceException in case a service has already been configured - */ - default public void applyDefault() throws ServiceException { - hideEnvironment(); - ignoreContentType(); - disableDebugging(); - ignoreUnsupportedPreferences(); - useTemporaryLocations(); - changeSystemLineSeparator(); - } -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleConfig.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleConfig.java deleted file mode 100644 index 8e4b12c8d0..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleConfig.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; - -/** A bundle configuration is given by its activator and the desired state */ -public abstract class BundleConfig> { - public static class Entry { - public final BundleActivator activator; - public final int state; - - public Entry(BundleActivator activator, int state) { - Objects.requireNonNull(activator, "activator"); - this.activator = activator; - this.state = state; - } - } - - private final List config; - - protected BundleConfig() { - config = new ArrayList(); - } - - /** - * Activate a bundle with a certain state. A non-active state is used by - * some bundles to allow a slim instantiation (for example in a headless - * Eclipse). - */ - public void add(BundleActivator activator, int state) { - config.add(new Entry(activator, state)); - } - - /** Returns the current configuration */ - public List get() { - return config; - } - - /** Activate a set of bundles with certain states */ - public void add(List config) { - this.config.addAll(config); - } - - /** Activate a bundle in active state, which is the nominal choice */ - public void add(BundleActivator activator) { - add(activator, Bundle.ACTIVE); - } - - /** Activate a set of bundles with in active state */ - public void add(Collection config) { - config.stream().forEach(entry -> add(entry)); - } - - /** Activate a default bundle with its default state */ - public void add(T bundle) { - add(create(bundle), getDefaultState(bundle)); - } - - /** Activate a set of default bundles with their default states */ - @SuppressWarnings("unchecked") - public void add(T... bundles) { - Arrays.asList(bundles).forEach(bundle -> add(bundle)); - } - - /** Activate a default bundle with a custom state */ - public void add(T bundle, int state) { - add(create(bundle), state); - } - - /** Applies the default configurations. */ - public abstract void applyDefault(); - - protected abstract BundleActivator create(T bundle); - - protected abstract int getDefaultState(T bundle); - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleController.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleController.java deleted file mode 100644 index 7c5311126e..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleController.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.osgi; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.eclipse.core.internal.jobs.JobManager; -import org.eclipse.osgi.internal.location.LocationHelper; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; -import org.osgi.framework.Filter; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.wiring.FrameworkWiring; - -/** - * OSGi bundle controller allowing a minimal Eclipse platform setup - * by bypassing the Eclipse internal platform. - * - * Bundles are loaded by the same class loader, sharing the same context. - * Services do not provide individual properties, but making use of the framework properties. - * All bundles have a persistent state. The OSGi life-cycle is not supported. - */ -public final class BundleController implements StaticBundleContext { - private static final String ECLIPSE_LAUNCHER_SYMBOLIC_NAME = "org.eclipse.osgi"; - - private final SimpleBundle systemBundle; - private final Map properties; - private final ServiceCollection services; - private final BundleSet bundles; - - @SuppressWarnings("deprecation") - public BundleController() throws BundleException { - //OSGI locks are not required, since this framework does not allow changes after initialization. - System.setProperty(LocationHelper.PROP_OSGI_LOCKING, LocationHelper.LOCKING_NONE); - - this.properties = new HashMap(); - //Don't activate all plugin bundles. Activation is triggered by this controller where needed. - properties.put(org.eclipse.core.internal.runtime.InternalPlatform.PROP_ACTIVATE_PLUGINS, Boolean.toString(false)); - - /* - * Used to set-up an internal member of the Eclipse runtime FindSupport, - * which is used during resources look-up from different version of bundles. - * Since the concept of the Spotless Eclipse framework does not allow multiple versions - * for a bundle, this property is never used. - */ - properties.put(org.eclipse.core.internal.runtime.FindSupport.PROP_NL, ""); - - bundles = new BundleSet(); - systemBundle = new SimpleBundle(this, Bundle.ACTIVE); - bundles.add(systemBundle); - - services = new ServiceCollection(systemBundle, properties); - - //Eclipse core (InternalPlatform) still uses PackageAdmin for looking up bundles - EclipseBundleLookup bundleLookup = new EclipseBundleLookup(systemBundle, bundles); - services.add(org.osgi.service.packageadmin.PackageAdmin.class, bundleLookup); - services.add(FrameworkWiring.class, bundleLookup); - - //Redirect framework activator requests to the the org.eclipse.osgi bundle to this instance. - bundles.add(new SimpleBundle(systemBundle, ECLIPSE_LAUNCHER_SYMBOLIC_NAME, Bundle.ACTIVE)); - FrameworkBundleRegistry.initialize(this); - } - - /** - * Stop {@link org.eclipse.core.internal.jobs.JobManager} worker pool - * and clean up resources of Spotless bundles and services. - * - * @implNote The {@link org.osgi.framework.BundleActivator}s - * are not stopped, since they are not completely started. - * For example services are suppressed by {@link StaticBundleContext}. - */ - public void shutdown() { - JobManager.shutdown(); - bundles.getAll().forEach(b -> { - try { - b.stop(); - } catch (IllegalStateException | BundleException e) { - //Stop on best effort basis - } - }); - services.stop(); - } - - public ServiceCollection getServices() { - return services; - } - - /** Adds and starts a new bundle. */ - public void addBundle(int bundleState, BundleActivator activator, Function register) throws BundleException { - BundleContext contextFacade = new BundleControllerContextFacade(this, bundleState, activator); - bundles.add(contextFacade.getBundle()); - BundleException exception = register.apply(contextFacade.getBundle()); - if (null != exception) - throw exception; - try { - activator.start(contextFacade); - } catch (Exception e) { - throw new BundleException(String.format("Failed do start %s.", activator.getClass().getName()), BundleException.ACTIVATOR_ERROR, e); - } - } - - /** Adds new bundel whithout activator (e.g. used for only for extensions) */ - public void addBundle(Class clazzInBundleJar, Function register) throws BundleException { - BundleContext contextFacade = new BundleControllerContextFacade(this, clazzInBundleJar); - bundles.add(contextFacade.getBundle()); - BundleException exception = register.apply(contextFacade.getBundle()); - if (null != exception) - throw exception; - } - - /** Creates a context with an individual state if required. */ - public BundleContext createContext(int state) { - if (state == systemBundle.getState()) { - return this; - } - return new BundleControllerContextFacade(systemBundle, state); - } - - @Override - public String getProperty(String key) { - return properties.get(key); - } - - @Override - public Bundle getBundle() { - return systemBundle; - } - - @Override - public Bundle[] getBundles() { - Collection shallowCopy = bundles.getAll(); - return shallowCopy.toArray(new Bundle[shallowCopy.size()]); - } - - @Override - public Bundle getBundle(long id) { - return bundles.get(id); - } - - @Override - public Bundle getBundle(String location) { - if (Constants.SYSTEM_BUNDLE_LOCATION.equals(location)) { - return systemBundle; - } - return null; - } - - @Override - public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - //Filters are based on class names - String interfaceClassName = (null == clazz) ? filter : clazz; - return services.getReferences(interfaceClassName); - } - - @Override - public S getService(ServiceReference reference) { - return services.getService(reference); - } - - @Override - public Filter createFilter(String filter) throws InvalidSyntaxException { - return services.createFilter(filter); - } - - /** - * Facade providing access to an existing controller for a bundle - *

- * All bundles have unrestricted access to the framework services and properties. - * However, each bundle and its context needs to maintain its individual - * symbolic name for look-up. - *

- */ - private static class BundleControllerContextFacade implements StaticBundleContext { - - private final BundleContext context; - private final Bundle bundle; - - private BundleControllerContextFacade(BundleContext context, int bundleState, BundleActivator activator) throws BundleException { - this.context = context; - bundle = new SimpleBundle(this, bundleState, activator); - } - - private BundleControllerContextFacade(BundleContext context, Class clazzInBundleJar) throws BundleException { - this.context = context; - bundle = new SimpleBundle(this, clazzInBundleJar); - } - - /** Fakes an individual bundle state */ - private BundleControllerContextFacade(SimpleBundle bundle, int bundleState) { - this.context = bundle.getBundleContext(); - this.bundle = new SimpleBundle(bundle, bundleState); - } - - @Override - public String getProperty(String key) { - return context.getProperty(key); - } - - @Override - public Bundle getBundle() { - return bundle; - } - - @Override - public Bundle[] getBundles() { - return context.getBundles(); - } - - @Override - public Bundle getBundle(long id) { - return context.getBundle(id); - } - - @Override - public Bundle getBundle(String location) { - return context.getBundle(location); - } - - @Override - public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - return context.getAllServiceReferences(clazz, filter); - } - - @Override - public S getService(ServiceReference reference) { - return context.getService(reference); - } - - @Override - public Filter createFilter(String filter) throws InvalidSyntaxException { - return context.createFilter(filter); - } - - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSet.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSet.java deleted file mode 100644 index 59e15929fd..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSet.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; - -/** Thread save set of bundles, whereas each bundle has a unique symbolic name and a unique ID. */ -class BundleSet { - private final Map symbolicName2bundle; - private final Map id2bundle; - - BundleSet() { - symbolicName2bundle = new ConcurrentHashMap(); - id2bundle = new ConcurrentHashMap(); - } - - /** Get all bundles in collection */ - Collection getAll() { - return symbolicName2bundle.values(); - } - - /** Get bundle by symbolic name or null if collection does not contain the corresponding bundle. */ - Bundle get(String symbolicName) { - return symbolicName2bundle.get(symbolicName); - } - - /** Get bundle by its ID or null if collection does not contain the corresponding bundle. */ - Bundle get(long id) { - return id2bundle.get(id); - } - - /** Add bundle to collection. - * @throws BundleException */ - void add(Bundle bundle) throws BundleException { - Bundle existingBundle = symbolicName2bundle.put(bundle.getSymbolicName(), bundle); - if (null != existingBundle) { - throw new BundleException( - String.format("Bundle '%s' (ID: %d) is already part of collection with ID %d.", - bundle.getSymbolicName(), bundle.getBundleId(), existingBundle.getBundleId()), - BundleException.DUPLICATE_BUNDLE_ERROR); - } - Bundle bundleWithSameID = id2bundle.put(bundle.getBundleId(), bundle); - if (null != bundleWithSameID) { - throw new BundleException( - String.format("Bundle ID '%d' for '%s' is already used by '%s'.", - bundle.getBundleId(), - bundle.getSymbolicName(), - bundleWithSameID.getSymbolicName()), - BundleException.DUPLICATE_BUNDLE_ERROR); - } - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/EclipseBundleLookup.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/EclipseBundleLookup.java deleted file mode 100644 index 0ec091a58b..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/EclipseBundleLookup.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.eclipse.osgi.internal.framework.FilterImpl; -import org.osgi.framework.Bundle; -import org.osgi.framework.FrameworkListener; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.namespace.IdentityNamespace; -import org.osgi.framework.wiring.BundleCapability; -import org.osgi.framework.wiring.FrameworkWiring; -import org.osgi.resource.Namespace; -import org.osgi.resource.Requirement; -import org.osgi.service.packageadmin.ExportedPackage; -import org.osgi.service.packageadmin.PackageAdmin; -import org.osgi.service.packageadmin.RequiredBundle; - -/** - * - * {@link PackageAdmin} and {@link FrameworkWiring} service for bundle look-up. - *

- * The wiring information will always claim that all required bundles are present, since - * Spotlss does on purpose not provide all dependencies requested by plugins, since - * only small parts of the plugins are used. - * Removal and addition requests for bundles will always claim that there is nothing to do. - *

- * PackageAdmin interface is deprecated, but might still be used by bundles. - * It is kept for backward compatibility until removed from Eclipse. - */ -@SuppressWarnings("deprecation") -class EclipseBundleLookup implements FrameworkWiring, PackageAdmin { - - private static final Set OSGI_KEYS_FOR_SYMBOLIC_NAMES = Collections.unmodifiableSet(Stream.of(IdentityNamespace.IDENTITY_NAMESPACE, IdentityNamespace.TYPE_BUNDLE).collect(Collectors.toSet())); - private final Bundle systemBundle; - private final BundleSet bundles; - - EclipseBundleLookup(final Bundle systemBundle, final BundleSet bundles) { - this.systemBundle = systemBundle; - this.bundles = bundles; - } - - @Override - @Deprecated - public ExportedPackage[] getExportedPackages(Bundle bundle) { - return null; - } - - @Override - @Deprecated - public ExportedPackage[] getExportedPackages(String name) { - return null; - } - - @Override - @Deprecated - public ExportedPackage getExportedPackage(String name) { - return null; - } - - @Override - @Deprecated - public void refreshPackages(Bundle[] bundles) {} - - @Override - public boolean resolveBundles(Bundle[] bundles) { - return true; //All required bundles can be considered available (to be confirmed by UT) - } - - @Override - public RequiredBundle[] getRequiredBundles(String symbolicName) { - return null; // No unresolved required bundles exist - } - - @Override - public Bundle[] getBundles(String symbolicName, String versionRange) { - //Bundles with different versions cannot be supported due to the usage of same class-loader - Bundle bundle = bundles.get(symbolicName); - return (null == bundle) ? null : new Bundle[]{bundle}; - } - - @Override - public Bundle[] getFragments(Bundle bundle) { - return null; //No fragments - } - - @Override - public Bundle[] getHosts(Bundle bundle) { - return null; //No fragments - } - - @Override - public Bundle getBundle(@SuppressWarnings("rawtypes") Class clazz) { - return bundles.get(clazz.getName()); - } - - @Override - public int getBundleType(Bundle bundle) { - return 0; //No fragments - } - - @Override - public Bundle getBundle() { - return systemBundle; - } - - @Override - public void refreshBundles(Collection bundles, FrameworkListener... listeners) { - //Spotless bundles cannot be loaded dynamically - } - - @Override - public boolean resolveBundles(Collection bundles) { - return true; - } - - @Override - public Collection getRemovalPendingBundles() { - return Collections.emptyList(); //Nothing to remove - } - - @Override - public Collection getDependencyClosure(Collection bundles) { - return Collections.emptyList(); //No dependencies - } - - @Override - public Collection findProviders(Requirement requirement) { - // requirement must not be null (according to interface description)! - String filterSpec = requirement.getDirectives().get(Namespace.REQUIREMENT_FILTER_DIRECTIVE); - if (null == filterSpec) { - throw new IllegalArgumentException("Requirement filter diretive '" + Namespace.REQUIREMENT_FILTER_DIRECTIVE + "' not found."); - } - try { - FilterImpl requirementFilter = FilterImpl.newInstance(filterSpec); - Collection requiredSymbolicNames = getRequestedSymbolicNames(requirementFilter); - Collection capabilities = new ArrayList(requiredSymbolicNames.size()); - requiredSymbolicNames.forEach(symbolicName -> { - Bundle bundle = bundles.get(symbolicName); - if (bundle != null) { - capabilities.add(new SimpleBundleCapability(bundle)); - } - }); - return capabilities; - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Filter specifiation invalid:\n" + filterSpec, e); - } - } - - /** - * Simplified parser irgnoreing the version. - * Parser is incomplete since it ignores the filter operation. - * It basicall implements the bespoke way Eclipse maps its old style bundle handling to OSGI. - */ - private static Collection getRequestedSymbolicNames(FilterImpl filter) { - List symbolicNames = filter.getStandardOSGiAttributes().entrySet().stream().filter(entry -> OSGI_KEYS_FOR_SYMBOLIC_NAMES.contains(entry.getKey())).map(entry -> entry.getValue()).collect(Collectors.toList()); - filter.getChildren().forEach(childFilter -> { - symbolicNames.addAll(getRequestedSymbolicNames(childFilter)); - }); - return symbolicNames; - } -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/FrameworkBundleRegistry.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/FrameworkBundleRegistry.java deleted file mode 100644 index 18d5c4bed4..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/FrameworkBundleRegistry.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016-2020 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.eclipse.base.osgi; - -import java.util.Optional; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; -import org.osgi.framework.connect.FrameworkUtilHelper; - -/** - * Framework bundle registry service for bundles which do not come with an activator. - * - * Instead of returning the system bundle, a new bundle is created - * to provide an individual resource accessor, so that - * the correct JAR is searched for relative resource paths. - * Note that this can also be used to override - * original resources by providing a resource in the - * corresponding fat JAR location. - */ -public class FrameworkBundleRegistry implements FrameworkUtilHelper { - static BundleController INSTANCE = null; - - static void initialize(BundleController bundleController) { - if (INSTANCE != null) { - throw new RuntimeException(FrameworkBundleRegistry.class.getName() + " already initialized."); - } - INSTANCE = bundleController; - } - - @Override - public Optional getBundle(Class classFromBundle) { - try { - return Optional.of(new SimpleBundle(INSTANCE, classFromBundle)); - } catch (BundleException e) { - //If the class cannot be associated to a JAR or fat JAR resource location, just retun null - } - return Optional.empty(); - } -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ResourceAccessor.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ResourceAccessor.java deleted file mode 100644 index b75a2835ac..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ResourceAccessor.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.osgi; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Enumeration; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -import org.eclipse.osgi.internal.debug.Debug; -import org.eclipse.osgi.storage.bundlefile.BundleEntry; -import org.eclipse.osgi.storage.bundlefile.BundleFile; -import org.eclipse.osgi.storage.bundlefile.DirBundleFile; -import org.eclipse.osgi.storage.bundlefile.ZipBundleFile; -import org.eclipse.osgi.util.ManifestElement; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; - -import com.diffplug.spotless.extra.eclipse.base.service.NoDebugging; - -/** - * Helper to access resources. - *

- * Spotless Eclipse formatters using in many cases fat JARs, compressing multiple - * bundles within one JAR. Their resources can get overridden in a fat JAR. - * Hence they provided under the bundle activators path name. - *

- *

- * The resource overriding prevention is required in case the Spotless - * Eclipse formatters integrate third party JARs, not available via M2. - * Additionally it can be used in case existing third party Eclipse plugins - * are mocked by Spotless, requiring the provision of multiple customized - * plugin information (META-INF, plugin.xml) within one JAR. - *

- */ -class ResourceAccessor { - private static final Debug NO_DEBUGGING = new Debug(new NoDebugging()); - private final String fatJarResourcePath; - private final BundleFile bundleFile; - - /** Resources are located in the SpotlessFramework JAR */ - ResourceAccessor() throws BundleException { - this(ResourceAccessor.class); - } - - /** Resources are located in the JAR of the given class - * @throws BundleException */ - ResourceAccessor(Class clazz) throws BundleException { - String bundleObjPath = clazz.getName(); - fatJarResourcePath = bundleObjPath.substring(0, bundleObjPath.lastIndexOf('.')); - try { - bundleFile = getBundlFile(clazz); - } catch (BundleException e) { - throw new BundleException(String.format("Failed to locate resources for bundle '%s'.", clazz.getName()), e); - } - } - - private static BundleFile getBundlFile(Class clazz) throws BundleException { - URI objUri = getBundleUri(clazz); - File jarOrDirectory = new File(objUri.getPath()); - if (!(jarOrDirectory.exists() && jarOrDirectory.canRead())) { - throw new BundleException(String.format("Path '%s' for '%s' is not accessible exist on local file system.", objUri, clazz.getName()), BundleException.READ_ERROR); - } - try { - return jarOrDirectory.isDirectory() ? new DirBundleFile(jarOrDirectory, false) : new ZipBundleFile(jarOrDirectory, null, null, NO_DEBUGGING, false); - } catch (IOException e) { - throw new BundleException(String.format("Cannot access bundle at '%s'.", jarOrDirectory), BundleException.READ_ERROR, e); - } - } - - private static URI getBundleUri(Class clazz) throws BundleException { - try { - URL objUrl = clazz.getProtectionDomain().getCodeSource().getLocation(); - return objUrl.toURI(); - } catch (NullPointerException e) { - //No bunlde should be used for RT classes lookup. See also org.eclipse.core.runtime.PerformanceStats. - throw new BundleException(String.format("No code source can be located for class '%s'. Class is probably not within a bundle, but part of the RT.", clazz.getName()), BundleException.READ_ERROR, e); - } catch (SecurityException e) { - throw new BundleException(String.format("Access to class '%s' is denied.", clazz.getName()), BundleException.READ_ERROR, e); - } catch (URISyntaxException e) { - throw new BundleException(String.format("Path for '%s' is invalid.", clazz.getName()), BundleException.READ_ERROR, e); - } - } - - /** Get the manifest name from the resources. */ - String getManifestName() throws BundleException { - URL manifestUrl = getEntry(JarFile.MANIFEST_NAME); - if (null != manifestUrl) { - try { - Manifest manifest = new Manifest(manifestUrl.openStream()); - String headerValue = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME); - if (null == headerValue) { - throw new BundleException(String.format("Symbolic values not found in '%s'.", manifestUrl), BundleException.MANIFEST_ERROR); - } - ManifestElement[] elements = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, headerValue); - if (null == elements) { - throw new BundleException(String.format("Symbolic name not found '%s'. Value is '%s'.", manifestUrl, headerValue), BundleException.MANIFEST_ERROR); - } - //The parser already checked that at least one value exists - return elements[0].getValueComponents()[0]; - - } catch (IOException e) { - throw new BundleException(String.format("Failed to parse Manifest '%s' in '%s'.", manifestUrl, bundleFile.toString()), BundleException.MANIFEST_ERROR, e); - } - } - throw new BundleException(String.format("'%s' in '%s' not found. Tried also fat JAR location '%s'.", JarFile.MANIFEST_NAME, bundleFile.toString(), fatJarResourcePath), BundleException.MANIFEST_ERROR); - } - - /** Get resource URL for relative path, or null if the path is not present */ - URL getEntry(String path) { - BundleEntry entry = bundleFile.getEntry(getFatJarPath(path)); - if (null == entry) { - entry = bundleFile.getEntry(path); - } - return null == entry ? null : entry.getLocalURL(); - } - - /** - * Enumeration of Strings that indicate the paths found or null if the path does not exist. - */ - Enumeration getEntries(String path) { - Enumeration entries = bundleFile.getEntryPaths(getFatJarPath(path)); - if (null == entries) { - entries = bundleFile.getEntryPaths(path); - } - return entries; - } - - private String getFatJarPath(String path) { - return fatJarResourcePath + "/" + path; - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollection.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollection.java deleted file mode 100644 index 2ea67f712f..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollection.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.osgi; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Dictionary; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.eclipse.osgi.internal.framework.DTOBuilder; -import org.osgi.framework.Bundle; -import org.osgi.framework.Filter; -import org.osgi.framework.ServiceException; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.dto.ServiceReferenceDTO; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; - -/** - * Collection of services. - * Eclipse service are not expected to hold any resources. Spotless services - * can implement AutoCloseable in case a resource release is required on shutdown. - * - * Note that the collection access is not thread save, since it is expected - * that the collection is completed before starting any bundles. - */ -public class ServiceCollection implements SpotlessEclipseServiceConfig { - private final Map> className2Service; - private final List servicesWithResources; - private final Bundle systemBundle; - private final Map properties; - - /** - * Collection of services - * @param systemBundle All services will belong to the system bundles - * @param All services share the same properties - */ - ServiceCollection(Bundle systemBundle, Map properties) { - className2Service = new HashMap>(); - servicesWithResources = new ArrayList<>(); - this.systemBundle = systemBundle; - this.properties = properties; - } - - void stop() { - servicesWithResources.stream().forEach(s -> { - try { - s.close(); - } catch (Exception e) { - //Stop on best effort basis - } - }); - } - - @Override - public void set(String key, String value) { - properties.put(key, value); - } - - @Override - public void add(Class interfaceClass, S service) throws ServiceException { - String className = interfaceClass.getName(); - if (null != className2Service.put(interfaceClass.getName(), new FrameworkServiceReference(className, service))) { - throw new ServiceException( - String.format("Service '%s' is already registered.", interfaceClass.getName()), ServiceException.FACTORY_ERROR); - } - if (service instanceof AutoCloseable) { - servicesWithResources.add((AutoCloseable) service); - } - } - - /** Creates filter object suitable to lookup service by its interface name. */ - Filter createFilter(String filterDescr) { - Optional serviceClassName = className2Service.keySet().stream().filter(serviceClazzName -> filterDescr.contains(serviceClazzName)).findFirst(); - return new ClassNameBasedFilter(serviceClassName); - } - - /** - * Get reference matching interface class name or all references - * @param interfaceClassName Class name filter. All references are returned in case filter is null - * @return Matching or all interfaces. If no interface is matching the filter, null is returned. - */ - ServiceReference[] getReferences(String interfaceClassName) { - if (null == interfaceClassName) { - Collection> allServices = className2Service.values(); - return allServices.toArray(new ServiceReference[allServices.size()]); - } - ServiceReference singleService = className2Service.get(interfaceClassName); - return (null == singleService) ? null : new ServiceReference[]{singleService}; - } - - /** - * Return service for reference if it belongs to the system bundle. - * @param reference Service reference - * @return null, if service does not belong to the system bundle - */ - S getService(ServiceReference reference) { - if (systemBundle == reference.getBundle()) { - return ((FrameworkServiceReference) reference).getService(); - } - return null; - } - - /** References to static services (not modifiable at run-time */ - private class FrameworkServiceReference implements ServiceReference { - - private final String className; - private final S service; - - private FrameworkServiceReference(String className, S service) { - this.className = className; - this.service = service; - } - - private S getService() { - return service; - } - - @Override - public java.lang.Object getProperty(String key) { - return properties.get(key); - } - - @Override - public String[] getPropertyKeys() { - return properties.keySet().toArray(new String[properties.size()]); - } - - @Override - public Bundle getBundle() { - return systemBundle; - } - - @Override - public Bundle[] getUsingBundles() { - return new Bundle[]{systemBundle}; - } - - @Override - public boolean isAssignableTo(Bundle bundle, String className) { - // Since only one class loader is used, same class come from the same package - return this.className.equals(className); - } - - @Override - @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS") - public int compareTo(Object reference) { - return (this == reference) ? 0 : 1; - } - - @Override - public Dictionary getProperties() { - return new Hashtable(properties); - } - - @SuppressWarnings("unchecked") - @Override - public A adapt(Class type) { - if (ServiceReferenceDTO.class.equals(type)) { - return (A) DTOBuilder.newServiceReferenceDTO(this); - } - return null; - } - - } - - /** - * Class name based service filter - *

- * Dictionary and capability look-ups are not supported and marked as deprecated. - */ - private class ClassNameBasedFilter implements Filter { - - private final static String NO_MATCH_CLASS_NAME = ""; - - private final String className; - - private ClassNameBasedFilter(Optional className) { - this.className = className.orElse(NO_MATCH_CLASS_NAME); - } - - @Override - public boolean match(ServiceReference reference) { - return reference.isAssignableTo(systemBundle, className); - } - - @Override - @Deprecated - public boolean match(Dictionary dictionary) { - throw new UnsupportedOperationException("Dictionary based service look-up is not supported."); - } - - @Override - @Deprecated - public boolean matchCase(Dictionary dictionary) { - throw new UnsupportedOperationException("Dictionary based service look-up is not supported."); - } - - @Override - @Deprecated - public boolean matches(Map map) { - throw new UnsupportedOperationException("Capability based service look-up is not supported."); - } - - @Override - public String toString() { - return className; - } - - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundle.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundle.java deleted file mode 100644 index bdf8888b30..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundle.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.net.URL; -import java.util.Enumeration; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; - -/** Fixed state simple bundle. */ -class SimpleBundle implements StaticBundle, TemporaryBundle { - private final String name; - private final int state; - private final BundleContext context; - private final int id; - private final ResourceAccessor resources; - - /** System bundle corresponding to the SpotlessFramework JARs manifest */ - SimpleBundle(BundleContext context, int state) throws BundleException { - this(context, state, new ResourceAccessor()); - } - - /** System bundle for a dedicated bundle activator */ - SimpleBundle(BundleContext context, int state, BundleActivator activator) throws BundleException { - this(context, state, new ResourceAccessor(activator.getClass())); - } - - /** System bundle providing only extensions and therefore does not require an activator */ - SimpleBundle(BundleContext context, Class clazzInBundleJar) throws BundleException { - //These bundles are always active (means that resources have been resolved) - this(context, Bundle.ACTIVE, new ResourceAccessor(clazzInBundleJar)); - } - - /** Internal constructor */ - private SimpleBundle(BundleContext context, int state, ResourceAccessor resources) throws BundleException { - this.state = state; - this.context = context; - this.resources = resources; - id = context.getBundles().length; - name = resources.getManifestName(); - } - - /** Additional bundle with a different symbolic name and state */ - SimpleBundle(SimpleBundle master, String name, int state) { - this.name = name; - this.state = state; - context = master.context; - resources = master.resources; - id = context.getBundles().length; - } - - /** Bundle clone with a different state */ - SimpleBundle(SimpleBundle master, int state) { - this.state = state; - context = master.context; - resources = master.resources; - id = master.id; - name = master.name; - } - - @Override - public A adapt(Class type) { - /* - * The adaptation is currently used by the InternalPlugin to get the framework wiring - * implementation from the system bundle. - * The original purpose to provide more specialized access to the Bundle object, - * seems not be used by Eclipse at all. - * Hence the call is mapped to old-style Eclipse services. - */ - try { - - ServiceReference[] references = context.getAllServiceReferences(type.getName(), ""); - if ((null != references) && (0 != references.length)) { - if (1 != references.length) { - throw new IllegalArgumentException("Multiple services found for " + type.getName()); //In Spotless services should always be unique - } - Object obj = context.getService(references[0]); - try { - return type.cast(obj); - } catch (ClassCastException e) { - throw new IllegalArgumentException("Received unexpected class for reference filter " + type.getName(), e); - } - } - return null; - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Unexpected syntax exception", e); //Should never be thrown by Spotless bundle controller - } - } - - @Override - public int getState() { - return state; - } - - @Override - public long getBundleId() { - return id; - } - - @Override - public ServiceReference[] getRegisteredServices() { - try { - return context.getAllServiceReferences(null, null); - } catch (InvalidSyntaxException e) { - throw new RuntimeException(e); //Filter 'null' is valid for 'select all'. - } - } - - @Override - public String getSymbolicName() { - return name; - } - - @Override - public BundleContext getBundleContext() { - return context; - } - - @Override - public Enumeration getEntryPaths(String path) { - return resources.getEntries(path); - } - - @Override - public URL getEntry(String path) { - return resources.getEntry(path); - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundleCapability.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundleCapability.java deleted file mode 100644 index 66f331807d..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/SimpleBundleCapability.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.osgi.framework.Bundle; -import org.osgi.framework.Version; -import org.osgi.framework.wiring.BundleCapability; -import org.osgi.framework.wiring.BundleRequirement; -import org.osgi.framework.wiring.BundleRevision; -import org.osgi.framework.wiring.BundleWiring; -import org.osgi.resource.Capability; -import org.osgi.resource.Requirement; - -/** - * Simplified bundle capability ignoring internal wiring and versions - *

- * Since multiple versions/implementations of bundles for the same - * capability is not supported a split of bundle capability and revision is not required. - */ -class SimpleBundleCapability implements BundleCapability, BundleRevision { - private final Bundle bundle; - - SimpleBundleCapability(Bundle bundle) { - this.bundle = bundle; - } - - @Override - public BundleRevision getRevision() { - return this; - } - - @Override - public String getNamespace() { - return this.getClass().getName(); //All bundles live in th same namespace - } - - @Override - public Map getDirectives() { - return Collections.emptyMap(); - } - - @Override - public Map getAttributes() { - return Collections.emptyMap(); - } - - @Override - public BundleRevision getResource() { - return this; - } - - @Override - public Bundle getBundle() { - return bundle; - } - - @Override - public String getSymbolicName() { - return bundle.getSymbolicName(); - } - - @Override - public Version getVersion() { - return bundle.getVersion(); - } - - @Override - public List getDeclaredCapabilities(String namespace) { - return Collections.emptyList(); - } - - @Override - public List getDeclaredRequirements(String namespace) { - return Collections.emptyList(); - } - - @Override - public int getTypes() { - return 0; //It does not matter whether this bunddle is a fragment of not since all bundles are initially provided - } - - @Override - public BundleWiring getWiring() { - return null; //No wiring information - } - - @Override - public List getCapabilities(String namespace) { - return Collections.emptyList(); - } - - @Override - public List getRequirements(String namespace) { - return Collections.emptyList(); - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundle.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundle.java deleted file mode 100644 index 282804a332..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundle.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.io.InputStream; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; -import org.osgi.framework.ServiceReference; - -/** - * Unmodifiable bundle with a fixed life-cycle. - *

- * All state related modifications are ignored. - * Installation related methods (update/uninstall) are unsupported. - * Unsupported methods are marked as deprecated and causing an exception. - */ -public interface StaticBundle extends Bundle { - - @Override - default public void start(int options) throws BundleException {} - - @Override - default public void start() throws BundleException {} - - @Override - default public void stop(int options) throws BundleException {} - - @Override - default public void stop() throws BundleException {} - - @Override - @Deprecated - default public void update(InputStream input) throws BundleException { - update(); - } - - @Override - @Deprecated - default public void update() throws BundleException { - throw new UnsupportedOperationException("Bundle modifications are not supported."); - } - - @Override - @Deprecated - default public void uninstall() throws BundleException { - throw new UnsupportedOperationException("Bundles cannot be uninstalled."); - } - - @Override - default public long getLastModified() { - return 0; - } - - @Override - @Deprecated - default public String getLocation() { - throw new UnsupportedOperationException("Bundle lookup by location only required for installation/update."); - } - - @Override - default public ServiceReference[] getServicesInUse() { - return getRegisteredServices(); //There is no distinction between available services and services in use. - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundleContext.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundleContext.java deleted file mode 100644 index 040ef98444..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/StaticBundleContext.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.io.File; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Dictionary; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.BundleListener; -import org.osgi.framework.FrameworkListener; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceListener; -import org.osgi.framework.ServiceObjects; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; - -/** - * Restriction of the {@link BundleContext} interface rejecting run-time provision of bundles. - * Services provided at run-time are ignored. - *

- * Multiple service instances per class are not supported, hence the services can be filtered by class name. - * Unsupported methods are marked as deprecated an causing an exception. - * Registration and removal of bundle/service listeners are ignored, since a run-time - * provision of bundles or services is not supported. - */ -interface StaticBundleContext extends BundleContext { - - @Override - @Deprecated - default public Bundle installBundle(String location, InputStream input) throws BundleException { - throw new UnsupportedOperationException("Run-time installation of bundles is not supported."); - } - - @Override - @Deprecated - default public Bundle installBundle(String location) throws BundleException { - throw new UnsupportedOperationException("Run-time installation of bundles is not supported."); - } - - @Override - default public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException {} - - @Override - default public void addServiceListener(ServiceListener listener) {} - - @Override - default public void removeServiceListener(ServiceListener listener) {} - - @Override - default public void addBundleListener(BundleListener listener) {} - - @Override - default public void removeBundleListener(BundleListener listener) {} - - @Override - default public void addFrameworkListener(FrameworkListener listener) {} - - @Override - default public void removeFrameworkListener(FrameworkListener listener) {} - - @Override - @Deprecated - default public ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties) { - throw new UnsupportedOperationException("Run-time provision of services is not supported."); - } - - @Override - @Deprecated - default public ServiceRegistration registerService(String clazz, Object service, Dictionary properties) { - return null; //Ignore additional services - } - - @Deprecated - @Override - default public ServiceRegistration registerService(Class clazz, S service, Dictionary properties) { - return null; //Ignore additional services - } - - @Override - @Deprecated - default public ServiceRegistration registerService(Class clazz, ServiceFactory factory, Dictionary properties) { - return null; //Ignore additional services - } - - @SuppressWarnings("unchecked") - @Override - default public ServiceReference getServiceReference(Class clazz) { - Objects.requireNonNull(clazz, "The class under whose name the service was registered must not be null."); - return (ServiceReference) getServiceReference(clazz.getName()); - } - - @Override - default public ServiceReference getServiceReference(String clazz) { - Objects.requireNonNull(clazz, "The class under whose name the service was registered must not be null."); - ServiceReference[] references; - try { - references = getServiceReferences(clazz, null); - } catch (InvalidSyntaxException e) { - throw new RuntimeException(e); //null is always valid - } - return (null == references) ? null : references[0]; - } - - @Override - default public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - return getAllServiceReferences(clazz, filter); //Services are always considered compatible - } - - @Override - default public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - //Filters are based on class names - ServiceReference reference = (null == clazz) ? getServiceReference(filter) : getServiceReference(clazz); - return (reference == null) ? null : new ServiceReference[]{reference}; - } - - @SuppressWarnings("unchecked") - @Override - default public Collection> getServiceReferences(Class clazz, String filter) throws InvalidSyntaxException { - ServiceReference[] references = getServiceReferences(clazz.getName(), filter); - Collection> result = new ArrayList>(0); - if (null != references) { - result = Arrays.stream(references).map(r -> (ServiceReference) r).collect(Collectors.toList()); - } - return result; - } - - @Override - default public boolean ungetService(ServiceReference reference) { - return true; //Services are persistent and never unregistered - } - - @Override - @Deprecated - default public ServiceObjects getServiceObjects(ServiceReference reference) { - throw new UnsupportedOperationException("Service specific objects are not supported."); - } - - @Override - @Deprecated - default public File getDataFile(String filename) { - throw new UnsupportedOperationException("Persistent data storage provision is handled by the Location service."); - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/TemporaryBundle.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/TemporaryBundle.java deleted file mode 100644 index 24cfadc8c2..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/TemporaryBundle.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.security.cert.X509Certificate; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.osgi.framework.Bundle; -import org.osgi.framework.Version; - -/** - * Temporary bundle loaded by common class loader and added temporarily. - * It shall not be recognized as a Eclipse plugin (providing plugin.xml) - * but as a mere OSGI bundle. - *

- * META data look-ups are not supported (only required for Eclipse plugins). - * Entry look-up in the bundle space (findEntries) is not supported. - * Installation related methods (update/uninstall) are not supported. - * Unsupported methods are marked as deprecated and causing an exception. - */ -public interface TemporaryBundle extends Bundle { - - @Override - default public Version getVersion() { - return Version.emptyVersion; //Cannot support multiple version using single class loader. - } - - @Override - default public int compareTo(Bundle o) { - //Symbolic name is sufficient to distinguish bundles - return getSymbolicName().compareTo(o.getSymbolicName()); - } - - @Override - default public A adapt(Class type) { - return null; //Adaptation is not successful - } - - @Override - @Deprecated - default public Dictionary getHeaders() { - throw new UnsupportedOperationException("Bundle META information is not available."); - } - - @Override - @Deprecated - default public Dictionary getHeaders(String locale) { - return getHeaders(); - } - - @Override - default public URL getResource(String name) { - return getClass().getClassLoader().getResource(name); - } - - @Override - default public Enumeration getResources(String name) throws IOException { - return getClass().getClassLoader().getResources(name); - } - - @Override - @Deprecated - default public Enumeration findEntries(String path, String filePattern, boolean recurse) { - return null; //Local JAR look-up are not supported per default - } - - @Override - default public Class loadClass(String name) throws ClassNotFoundException { - return getClass().getClassLoader().loadClass(name); - } - - @Override - default public boolean hasPermission(Object permission) { - return true; //Dedicated permissions are not supported - } - - @Override - default public Map> getSignerCertificates(int signersType) { - return new HashMap>(0); //Bundle is not signed - } - - @Override - default public File getDataFile(String filename) { - return null; //No file system support for persistent files - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/PluginRegistrar.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/PluginRegistrar.java deleted file mode 100644 index cdee9893dc..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/PluginRegistrar.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.runtime; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.PropertyResourceBundle; -import java.util.ResourceBundle; - -import org.eclipse.core.internal.registry.ExtensionRegistry; -import org.eclipse.core.runtime.IContributor; -import org.eclipse.core.runtime.IExtensionRegistry; -import org.eclipse.core.runtime.RegistryFactory; -import org.eclipse.core.runtime.spi.RegistryContributor; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; - -/** Registers Eclipse plugins at runtime based on a loaded bundle. - *

- * Note that the functionality provided by this class uses incubation features of the Eclipse core. - */ -public class PluginRegistrar { - private static final String PLUGIN_XML = "plugin.xml"; - private static final String PLUGIN_PROPERTIES = "plugin.properties"; - - public static BundleException register(Bundle bundle) { - PluginRegistrar registrar = new PluginRegistrar(bundle); - try { - registrar.register(); - } catch (BundleException e) { - return e; - } - return null; - } - - private final Bundle bundle; - - private PluginRegistrar(Bundle bundle) { - this.bundle = bundle; - } - - private void register() throws BundleException { - IExtensionRegistry reg = RegistryFactory.getRegistry(); - Object registryUser = ((ExtensionRegistry) reg).getTemporaryUserToken(); - if (!reg.addContribution(getStreamForEntry(PLUGIN_XML), createContributor(), false, null, getPluginProperties(), registryUser)) { - throw new BundleException("Could not add plugin: " + bundle.getSymbolicName(), BundleException.REJECTED_BY_HOOK); - } - } - - private IContributor createContributor() { - return new RegistryContributor( - Long.toString(bundle.getBundleId()), - bundle.getSymbolicName(), - // Local host - null, - null); - } - - private ResourceBundle getPluginProperties() throws BundleException { - //Some plugins, like the org.codehaus.groovy.eclipse.core, do not provide a property file. - InputStream is = entryExists(PLUGIN_PROPERTIES) ? getStreamForEntry(PLUGIN_PROPERTIES) : new ByteArrayInputStream(new byte[0]); - try { - return new PropertyResourceBundle(is); - } catch (IOException e) { - throw new BundleException(String.format("Bund resource '%s' is not encoded with ISO-8859-1.", PLUGIN_PROPERTIES), BundleException.MANIFEST_ERROR, e); - } - } - - private InputStream getStreamForEntry(String path) throws BundleException { - try { - return getEntry(path).openStream(); - } catch (IOException e) { - throw new BundleException(String.format("Cannot access mandatory resource '%s'.", path), BundleException.MANIFEST_ERROR, e); - } - } - - private URL getEntry(String path) throws BundleException { - URL url = bundle.getEntry(path); - if (null == url) { - throw new BundleException(String.format("Cannot find mandatory resource '%s'.", path), BundleException.MANIFEST_ERROR); - } - return url; - } - - private boolean entryExists(String path) { - return null != bundle.getEntry(path); - } -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/HiddenEnvironment.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/HiddenEnvironment.java deleted file mode 100644 index 8c9c1a6c91..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/HiddenEnvironment.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.service; - -import java.util.Locale; - -import org.eclipse.osgi.service.environment.Constants; -import org.eclipse.osgi.service.environment.EnvironmentInfo; - -/** Empty default Eclipse environment. No system information is accessible. */ -public class HiddenEnvironment implements EnvironmentInfo { - - @Override - public String[] getCommandLineArgs() { - return new String[0]; - } - - @Override - public String[] getFrameworkArgs() { - return new String[0]; - } - - @Override - public String[] getNonFrameworkArgs() { - return new String[0]; - } - - @Override - public String getOSArch() { - return System.getProperty("os.arch"); - } - - @Override - public String getNL() { - return Locale.getDefault().getLanguage(); - } - - @Override - public String getOS() { - return Constants.OS_UNKNOWN; - } - - @Override - public String getWS() { - return null; //No window system - } - - @Override - public boolean inDebugMode() { - return false; - } - - @Override - public boolean inDevelopmentMode() { - return false; - } - - @Override - public String setProperty(String key, String value) { - return value; //Launcher information is not stored - } - - @Override - public String getProperty(String key) { - return null; //Launcher information/configuration is not required - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoContentTypeSpecificHandling.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoContentTypeSpecificHandling.java deleted file mode 100644 index 87e1f2f91b..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoContentTypeSpecificHandling.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.service; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.QualifiedName; -import org.eclipse.core.runtime.content.IContentDescription; -import org.eclipse.core.runtime.content.IContentType; -import org.eclipse.core.runtime.content.IContentTypeManager; -import org.eclipse.core.runtime.content.IContentTypeMatcher; -import org.eclipse.core.runtime.preferences.IScopeContext; - -/** No content type specific handling is supported. */ -public class NoContentTypeSpecificHandling implements IContentTypeManager { - - @Override - public IContentType findContentTypeFor(InputStream contents, String fileName) throws IOException { - return null; - } - - @Override - public IContentType findContentTypeFor(String fileName) { - return null; - } - - @Override - public IContentType[] findContentTypesFor(InputStream contents, String fileName) throws IOException { - return null; - } - - @Override - public IContentType[] findContentTypesFor(String fileName) { - return null; - } - - @Override - public IContentDescription getDescriptionFor(InputStream contents, String fileName, QualifiedName[] options) throws IOException { - return null; - } - - @Override - public IContentDescription getDescriptionFor(Reader contents, String fileName, QualifiedName[] options) throws IOException { - return null; - } - - @Override - public void addContentTypeChangeListener(IContentTypeChangeListener listener) {} - - @Override - public IContentType[] getAllContentTypes() { - return null; - } - - @Override - public IContentType getContentType(String contentTypeIdentifier) { - return null; - } - - @Override - public IContentTypeMatcher getMatcher(ISelectionPolicy customPolicy, IScopeContext context) { - return null; - } - - @Override - public void removeContentTypeChangeListener(IContentTypeChangeListener listener) { - - } - - @Override - public IContentType addContentType(String contentTypeIdentifier, String name, IContentType baseType) - throws CoreException { - return null; - } - - @Override - public void removeContentType(String contentTypeIdentifier) throws CoreException {} - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoDebugging.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoDebugging.java deleted file mode 100644 index 095be889e9..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoDebugging.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.service; - -import java.io.File; -import java.util.Map; - -import org.eclipse.osgi.service.debug.DebugOptions; -import org.eclipse.osgi.service.debug.DebugTrace; - -/** No debugging shall be performed */ -public class NoDebugging implements DebugOptions { - - @Override - public boolean getBooleanOption(String option, boolean defaultValue) { - return false; - } - - @Override - public String getOption(String option) { - return null; - } - - @Override - public String getOption(String option, String defaultValue) { - return null; - } - - @Override - public int getIntegerOption(String option, int defaultValue) { - return 0; - } - - @Override - public Map getOptions() { - return null; - } - - @Override - public void setOption(String option, String value) {} - - @Override - public void setOptions(Map options) {} - - @Override - public void removeOption(String option) {} - - @Override - public boolean isDebugEnabled() { - return false; - } - - @Override - public void setDebugEnabled(boolean value) {} - - @Override - public void setFile(File newFile) {} - - @Override - public File getFile() { - return null; - } - - @Override - public DebugTrace newDebugTrace(String bundleSymbolicName) { - return null; - } - - @Override - public DebugTrace newDebugTrace(String bundleSymbolicName, Class traceEntryClass) { - return null; - } - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoEclipsePreferences.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoEclipsePreferences.java deleted file mode 100644 index 7f6bed44ed..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/NoEclipsePreferences.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.service; - -import java.io.InputStream; -import java.io.OutputStream; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.preferences.DefaultScope; -import org.eclipse.core.runtime.preferences.IEclipsePreferences; -import org.eclipse.core.runtime.preferences.IExportedPreferences; -import org.eclipse.core.runtime.preferences.IPreferenceFilter; -import org.eclipse.core.runtime.preferences.IPreferencesService; -import org.eclipse.core.runtime.preferences.IScopeContext; -import org.osgi.service.prefs.Preferences; - -/** - * The formatters dependents on plugins which access Eclipse core functionality, - * configurable by preferences. This functionality is never used by the formatters itself. - * Hence modifications are ignored and default values are provided on request. - */ -public class NoEclipsePreferences implements IPreferencesService { - private static final String UNUSED = "unused"; - - @Override - public IEclipsePreferences getRootNode() { - //Return value is not effectively used. - return DefaultScope.INSTANCE.getNode(UNUSED); - } - - @Override - public String get(String key, String defaultValue, Preferences[] nodes) { - return null; - } - - @Override - public boolean getBoolean(String qualifier, String key, boolean defaultValue, IScopeContext[] contexts) { - return false; - } - - @Override - public byte[] getByteArray(String qualifier, String key, byte[] defaultValue, IScopeContext[] contexts) { - return null; - } - - @Override - public double getDouble(String qualifier, String key, double defaultValue, IScopeContext[] contexts) { - return 0; - } - - @Override - public float getFloat(String qualifier, String key, float defaultValue, IScopeContext[] contexts) { - return 0; - } - - @Override - public int getInt(String qualifier, String key, int defaultValue, IScopeContext[] contexts) { - return 0; - } - - @Override - public long getLong(String qualifier, String key, long defaultValue, IScopeContext[] contexts) { - return 0; - } - - @Override - public String getString(String qualifier, String key, String defaultValue, IScopeContext[] contexts) { - return null; - } - - @Override - public IStatus exportPreferences(IEclipsePreferences node, OutputStream output, String[] excludesList) throws CoreException { - return null; - } - - @Override - public IStatus importPreferences(InputStream input) throws CoreException { - return null; - } - - @Override - public IStatus applyPreferences(IExportedPreferences preferences) throws CoreException { - return null; - } - - @Override - public IExportedPreferences readPreferences(InputStream input) throws CoreException { - return null; - } - - @Override - public String[] getDefaultLookupOrder(String qualifier, String key) { - return null; - } - - @Override - public String[] getLookupOrder(String qualifier, String key) { - return null; - } - - @Override - public void setDefaultLookupOrder(String qualifier, String key, String[] order) {} - - @Override - public void exportPreferences(IEclipsePreferences node, IPreferenceFilter[] filters, OutputStream output) throws CoreException {} - - @Override - public IPreferenceFilter[] matches(IEclipsePreferences node, IPreferenceFilter[] filters) throws CoreException { - return null; - } - - @Override - public void applyPreferences(IEclipsePreferences node, IPreferenceFilter[] filters) throws CoreException {} - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/SingleSlf4JService.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/SingleSlf4JService.java deleted file mode 100644 index 6928621c83..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/SingleSlf4JService.java +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.service; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import javax.annotation.Nullable; - -import org.eclipse.core.internal.runtime.InternalPlatform; -import org.eclipse.equinox.log.ExtendedLogReaderService; -import org.eclipse.equinox.log.ExtendedLogService; -import org.eclipse.equinox.log.LogFilter; -import org.eclipse.equinox.log.Logger; -import org.osgi.framework.Bundle; -import org.osgi.framework.ServiceReference; -import org.osgi.service.log.LogEntry; -import org.osgi.service.log.LogLevel; -import org.osgi.service.log.LogListener; -import org.osgi.service.log.LoggerConsumer; - -/** - * Eclipse log service facade that delegates to a Slf4J logger. - * The service does not provide historical log entries (empty history reported). - * No factory service is provided for OSGI logger extensions (method are marked - * as deprecated and raise UnsupportedOperationException). - * All Eclipse logger are delegated to a single Slf4J logger instance. - * The log messages can be formatted by customizer. - */ -public class SingleSlf4JService implements ExtendedLogService, ExtendedLogReaderService { - - private final org.slf4j.Logger delegate; - private final Map logLevel2methods; - private final Set listener; - private final BiFunction messageCustomizer; - - /** Create facade for named logger with customized messages. */ - public SingleSlf4JService(String name, BiFunction messageCustomizer) { - delegate = org.slf4j.LoggerFactory.getLogger(name); - logLevel2methods = new HashMap(); - /* - * Audit message are treated as normal info-messages and might not get logged. - * Logging of Eclipse messages in Spotless formatter is meant for debugging purposes and - * detection of erroneous usage/override of internal Eclipse methods. - * Hence the concept of Audit is not required. - */ - logLevel2methods.put(LogLevel.AUDIT, - create(() -> true, m -> delegate.info(m), (m, e) -> delegate.info(m, e))); - logLevel2methods.put(LogLevel.DEBUG, - create(() -> delegate.isDebugEnabled(), m -> delegate.debug(m), (m, e) -> delegate.debug(m, e))); - logLevel2methods.put(LogLevel.ERROR, - create(() -> delegate.isErrorEnabled(), m -> delegate.error(m), (m, e) -> delegate.error(m, e))); - logLevel2methods.put(LogLevel.INFO, - create(() -> delegate.isInfoEnabled(), m -> delegate.info(m), (m, e) -> delegate.info(m, e))); - logLevel2methods.put(LogLevel.TRACE, - create(() -> delegate.isTraceEnabled(), m -> delegate.trace(m), (m, e) -> delegate.trace(m, e))); - logLevel2methods.put(LogLevel.WARN, - create(() -> delegate.isWarnEnabled(), m -> delegate.warn(m), (m, e) -> delegate.warn(m, e))); - listener = new HashSet(); - this.messageCustomizer = messageCustomizer; - } - - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public void log(int level, String message) { - log(this, level, message); - } - - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public void log(int level, String message, @Nullable Throwable exception) { - log(this, level, message, exception); - } - - @SuppressWarnings("rawtypes") - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public void log(ServiceReference sr, int level, String message) { - log(this, level, message); - } - - @SuppressWarnings("rawtypes") - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public void log(ServiceReference sr, int level, String message, Throwable exception) { - log(this, level, message, exception); - } - - @Override - public void log(Object context, int level, String message) { - log(context, level, message, null); - } - - @Override - public void log(Object context, int level, String message, @Nullable Throwable exception) { - LogLevel logLevel = convertDeprectatedOsgiLevel(level); - log(new SimpleLogEntry(logLevel, message, exception)); - } - - @Override - public boolean isLoggable(int level) { - LogLevel logLevel = convertDeprectatedOsgiLevel(level); - return logLevel2methods.get(logLevel).isEnabled(); - } - - @SuppressWarnings("deprecation") ////Backward compatibility with Eclipse OSGI 3.12 - private static LogLevel convertDeprectatedOsgiLevel(int level) { - switch (level) { - case SingleSlf4JService.LOG_DEBUG: - return LogLevel.DEBUG; - case SingleSlf4JService.LOG_INFO: - return LogLevel.INFO; - case SingleSlf4JService.LOG_ERROR: - return LogLevel.ERROR; - case SingleSlf4JService.LOG_WARNING: - return LogLevel.WARN; - default: - return LogLevel.AUDIT; - } - } - - @Override - public String getName() { - return delegate.getName(); - } - - @Override - public Logger getLogger(String loggerName) { - return this; - } - - @Override - public Logger getLogger(Bundle bundle, String loggerName) { - return this; - } - - @Override - public void addLogListener(LogListener listener) { - synchronized (this.listener) { - this.listener.add(listener); - } - } - - @Override - public void removeLogListener(LogListener listener) { - synchronized (this.listener) { - this.listener.remove(listener); - } - } - - private void log(LogEntry entry) { - synchronized (listener) { - listener.stream().forEach(l -> l.logged(entry)); - } - String customMessage = messageCustomizer.apply(entry.getMessage(), entry.getLogLevel()); - logLevel2methods.get(entry.getLogLevel()).log(customMessage, entry.getException()); - } - - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public Enumeration getLog() { - return Collections.emptyEnumeration(); //We do not provide historical information - } - - @Override - public void addLogListener(LogListener listener, LogFilter filter) { - addLogListener(listener); //Listener must filter if required - - } - - @Override - public org.osgi.service.log.Logger getLogger(Class clazz) { - return this; - } - - @Override - @Deprecated - public L getLogger(String name, Class loggerType) { - throw new UnsupportedOperationException("Logger factory for indifivaul types currently not supported."); - } - - @Override - @Deprecated - public L getLogger(Class clazz, Class loggerType) { - return getLogger(getName(), loggerType); - } - - @Override - @Deprecated - public L getLogger(Bundle bundle, String name, Class loggerType) { - return getLogger(getName(), loggerType); - } - - @Override - public boolean isTraceEnabled() { - return delegate.isTraceEnabled(); - } - - @Override - public void trace(String message) { - log(new SimpleLogEntry(LogLevel.TRACE, message)); - } - - @Override - public void trace(String format, Object arg) { - trace(String.format(format, arg)); - } - - @Override - public void trace(String format, Object arg1, Object arg2) { - trace(String.format(format, arg1, arg2)); - } - - @Override - public void trace(String format, Object... arguments) { - trace(String.format(format, arguments)); - } - - @Override - public void trace(LoggerConsumer consumer) throws E { - consumer.accept(this); - } - - @Override - public boolean isDebugEnabled() { - return delegate.isDebugEnabled(); - } - - @Override - public void debug(String message) { - log(new SimpleLogEntry(LogLevel.DEBUG, message)); - } - - @Override - public void debug(String format, Object arg) { - debug(String.format(format, arg)); - } - - @Override - public void debug(String format, Object arg1, Object arg2) { - debug(String.format(format, arg1, arg2)); - } - - @Override - public void debug(String format, Object... arguments) { - debug(String.format(format, arguments)); - } - - @Override - public void debug(LoggerConsumer consumer) throws E { - consumer.accept(this); - } - - @Override - public boolean isInfoEnabled() { - return delegate.isInfoEnabled(); - } - - @Override - public void info(String message) { - log(new SimpleLogEntry(LogLevel.INFO, message)); - } - - @Override - public void info(String format, Object arg) { - info(String.format(format, arg)); - } - - @Override - public void info(String format, Object arg1, Object arg2) { - info(String.format(format, arg1, arg2)); - } - - @Override - public void info(String format, Object... arguments) { - info(String.format(format, arguments)); - } - - @Override - public void info(LoggerConsumer consumer) throws E { - consumer.accept(this); - } - - @Override - public boolean isWarnEnabled() { - return delegate.isWarnEnabled(); - } - - @Override - public void warn(String message) { - log(new SimpleLogEntry(LogLevel.WARN, message)); - } - - @Override - public void warn(String format, Object arg) { - warn(String.format(format, arg)); - } - - @Override - public void warn(String format, Object arg1, Object arg2) { - warn(String.format(format, arg1, arg2)); - } - - @Override - public void warn(String format, Object... arguments) { - warn(String.format(format, arguments)); - } - - @Override - public void warn(LoggerConsumer consumer) throws E { - consumer.accept(this); - } - - @Override - public boolean isErrorEnabled() { - return delegate.isErrorEnabled(); - } - - @Override - public void error(String message) { - log(new SimpleLogEntry(LogLevel.ERROR, message)); - } - - @Override - public void error(String format, Object arg) { - error(String.format(format, arg)); - } - - @Override - public void error(String format, Object arg1, Object arg2) { - error(String.format(format, arg1, arg2)); - } - - @Override - public void error(String format, Object... arguments) { - error(String.format(format, arguments)); - } - - @Override - public void error(LoggerConsumer consumer) throws E { - consumer.accept(this); - } - - @Override - public void audit(String message) { - log(new SimpleLogEntry(LogLevel.AUDIT, message)); - } - - @Override - public void audit(String format, Object arg) { - audit(String.format(format, arg)); - } - - @Override - public void audit(String format, Object arg1, Object arg2) { - audit(String.format(format, arg1, arg2)); - } - - @Override - public void audit(String format, Object... arguments) { - audit(String.format(format, arguments)); - } - - /** Internal wrapper for Eclipse OSGI 3.12 based logs and new log services. */ - private static class SimpleLogEntry implements LogEntry { - - private final LogLevel level; - private final String message; - private final Optional execption; - - public SimpleLogEntry(LogLevel level, String message) { - this(level, message, Optional.empty()); - } - - public SimpleLogEntry(LogLevel level, String message, @Nullable Throwable execption) { - this(level, message, Optional.ofNullable(execption)); - } - - private SimpleLogEntry(LogLevel level, String message, Optional execption) { - this.level = level; - this.message = message; - this.execption = execption; - } - - @Override - public Bundle getBundle() { - //Return the spotless framework bundle - return InternalPlatform.getDefault().getBundleContext().getBundle(); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public ServiceReference getServiceReference() { - return null; - } - - @Override - @Deprecated - //Backward compatibility with Eclipse OSGI 3.12 - public int getLevel() { - switch (level) { - case DEBUG: - case TRACE: - return SingleSlf4JService.LOG_DEBUG; - case AUDIT: - case INFO: - return SingleSlf4JService.LOG_INFO; - case ERROR: - return SingleSlf4JService.LOG_ERROR; - case WARN: - return SingleSlf4JService.LOG_WARNING; - } - return SingleSlf4JService.LOG_ERROR; //Don't fail here. Just log it as error. This is anyway just for debugging internal problems. - } - - @Override - public String getMessage() { - return message; - } - - @Override - public Throwable getException() { - return execption.orElse(null); - } - - @Override - public long getTime() { - return 0; - } - - @Override - public String toString() { - StringWriter result = new StringWriter(); - result.write(message); - if (execption.isPresent()) { - result.write('\n'); - result.write(execption.get().toString()); - result.write('\n'); - execption.get().printStackTrace(new PrintWriter(result)); - } - return result.toString(); - } - - @Override - public LogLevel getLogLevel() { - return level; - } - - @Override - public String getLoggerName() { - return this.getClass().getSimpleName(); - } - - @Override - public long getSequence() { - return 0; - } - - @Override - public String getThreadInfo() { - return null; - } - - @Override - public StackTraceElement getLocation() { - return null; // Not used by SingleSlf4JService - } - - } - - private static LogMethods create(Supplier enabled, Consumer log, BiConsumer logException) { - return new LogMethods(enabled, log, logException); - } - - private static class LogMethods { - private final Supplier enabled; - private final Consumer log; - private final BiConsumer logException; - - private LogMethods(Supplier enabled, Consumer log, BiConsumer logException) { - this.enabled = enabled; - this.log = log; - this.logException = logException; - } - - public boolean isEnabled() { - return enabled.get(); - } - - public void log(String message) { - log.accept(message); - } - - public void log(String message, @Nullable Throwable exception) { - if (null == exception) { - log(message); - } else { - logException.accept(message, exception); - } - } - - }; - -} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/TemporaryLocation.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/TemporaryLocation.java deleted file mode 100644 index 2999014d23..0000000000 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/TemporaryLocation.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.service; - -import java.io.File; -import java.io.IOError; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Comparator; - -import org.eclipse.osgi.service.datalocation.Location; - -/** All files generated at runtime are stored in a temporary location. */ -public class TemporaryLocation implements Location, AutoCloseable { - private static final String TEMP_PREFIX = "com_diffplug_spotless_extra_eclipse"; - private final URL location; - private Location parent; - - public TemporaryLocation() { - this(null, createTemporaryDirectory()); - } - - private TemporaryLocation(Location parent, URL defaultValue) { - this.location = defaultValue; - this.parent = parent; - } - - private static URL createTemporaryDirectory() { - try { - Path location = Files.createTempDirectory(TEMP_PREFIX); - return location.toUri().toURL(); - } catch (IOException e) { - throw new IOError(e); - } - } - - @Override - public boolean allowsDefault() { - return false; - } - - @Override - public URL getDefault() { - return null; - } - - @Override - public Location getParentLocation() { - return parent; - } - - @Override - public URL getURL() { - return location; - } - - @Override - public boolean isSet() { - return true; - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - @Deprecated - public boolean setURL(URL value, boolean lock) throws IllegalStateException { - throw new IllegalStateException("URL not modifyable."); - } - - @Override - public boolean set(URL value, boolean lock) throws IllegalStateException, IOException { - throw new IllegalStateException("URL not modifyable."); - } - - @Override - public boolean set(URL value, boolean lock, String lockFilePath) throws IllegalStateException, IOException { - throw new IllegalStateException("URL not modifyable."); - } - - @Override - public boolean lock() throws IOException { - return false; //Lock not supported - } - - @Override - public void release() { - //Lock not supported - } - - @Override - public boolean isLocked() throws IOException { - return false; //Lock not supported - } - - @Override - public Location createLocation(Location parent, URL defaultValue, boolean readonly) { - return new TemporaryLocation(parent, defaultValue); - } - - @Override - public URL getDataArea(String path) throws IOException { - try { - Path locationPath = Paths.get(location.toURI()); - return locationPath.resolve(path).toUri().toURL(); - } catch (URISyntaxException e) { - throw new IOException("Location not correctly formatted.", e); - } - } - - @Override - public void close() throws Exception { - try { - Path path = Path.of(location.toURI()); - Files.walk(path) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); - path.toFile().delete(); - } catch (IOException e) { - //At shutdown everything is just done on best-efforts basis - } - } - -} diff --git a/_ext/eclipse-base/src/main/resources/META-INF/MANIFEST.MF b/_ext/eclipse-base/src/main/resources/META-INF/MANIFEST.MF deleted file mode 100644 index 348dc4c453..0000000000 --- a/_ext/eclipse-base/src/main/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-SymbolicName: com.diffplug.gradle.spotless.eclipse; singleton:=true diff --git a/_ext/eclipse-base/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper b/_ext/eclipse-base/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper deleted file mode 100644 index 0bd6c29641..0000000000 --- a/_ext/eclipse-base/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper +++ /dev/null @@ -1 +0,0 @@ -com.diffplug.spotless.extra.eclipse.base.osgi.FrameworkBundleRegistry \ No newline at end of file diff --git a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFrameworkTest.java b/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFrameworkTest.java deleted file mode 100644 index ca76c92ce1..0000000000 --- a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFrameworkTest.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.ObjectArrayAssert; -import org.assertj.core.api.StringAssert; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.ILog; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.equinox.log.ExtendedLogReaderService; -import org.eclipse.equinox.log.ExtendedLogService; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.ServiceReference; -import org.osgi.service.log.LogEntry; -import org.osgi.service.log.LogLevel; -import org.osgi.service.log.LogListener; -import org.slf4j.simple.SimpleLogger; - -import com.diffplug.spotless.extra.eclipse.base.service.SingleSlf4JService; - -/** Integration tests */ -class SpotlessEclipseFrameworkTest { - - private final static String TEST_LOGGER_NAME = SpotlessEclipseFrameworkTest.class.getSimpleName(); - private final static String CUSTOM_PREFIX = "prefix\t"; - private final static String CUSTOM_POSTFIX = "\tpostfix"; - private final static String TEST_EXCEPTION_MESSAGE = "MY TEST-EXCEPTION"; - private static Slf4JMesssageListener SLF4J_RECEIVER = null; - - private static boolean TREAT_ERROR_AS_EXCEPTION = false; - - @BeforeAll - static void frameworkTestSetup() throws BundleException { - //Prepare interception of SLF4J messages to System.out - SLF4J_RECEIVER = new Slf4JMesssageListener(); - - //Configure SLF4J-Simple - System.setProperty(SimpleLogger.LOG_FILE_KEY, "System.out"); - System.setProperty(SimpleLogger.SHOW_SHORT_LOG_NAME_KEY, "true"); - System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "warn"); - - //Instantiate default framework + SLF4J logger - SpotlessEclipseFramework.setup(new SpotlessEclipseConfig() { - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(TEST_LOGGER_NAME, (s, l) -> { - if (TREAT_ERROR_AS_EXCEPTION && (LogLevel.ERROR == l)) { - throw new IllegalArgumentException(TEST_EXCEPTION_MESSAGE); - } - return CUSTOM_PREFIX + s + CUSTOM_POSTFIX; - }); - } - }); - } - - @AfterAll - static void deregisterSlf4JReceiver() { - SLF4J_RECEIVER.deregister(); - } - - private EclipseMessageListener eclipseMessageListener; - - @BeforeEach - void eclipseReceiverSetup() { - ExtendedLogReaderService service = getService(ExtendedLogReaderService.class); - eclipseMessageListener = new EclipseMessageListener(); - service.addLogListener(eclipseMessageListener); - SLF4J_RECEIVER.clear(); - } - - @AfterEach - void logReceiverTearDown() { - ExtendedLogReaderService service = getService(ExtendedLogReaderService.class); - service.removeLogListener(eclipseMessageListener); - } - - @Test - void testCustomizedLogMessage() { - ResourcesPlugin.getPlugin().getLog().log(new Status(IStatus.ERROR, "Some plugin", "Hello World!")); - assertSlf4J() - .as("Error message logged.").received("Hello World!").contains(TEST_LOGGER_NAME) - .as("Customization method has been applied.").contains(CUSTOM_PREFIX, CUSTOM_POSTFIX) - .as("Status level has been converted to simple SLF4J level").contains("ERROR"); - assertEclipse() - .as("Warning message received.").received("Hello World!") - .as("Customization method is only for SLF4J").doesNotContain(CUSTOM_PREFIX); - } - - @Test - void testCustomizedLogException() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> { - try { - TREAT_ERROR_AS_EXCEPTION = true; - ResourcesPlugin.getPlugin().getLog().log(new Status(IStatus.ERROR, "Some plugin", "Hello World!")); - } finally { - TREAT_ERROR_AS_EXCEPTION = false; - } - }) - .withMessage(TEST_EXCEPTION_MESSAGE); - } - - @Test - void testPluginLog() { - List logLevels = Arrays.asList(IStatus.CANCEL, IStatus.ERROR, IStatus.INFO, IStatus.OK, IStatus.WARNING); - List enabledLogLevels = Arrays.asList(IStatus.ERROR, IStatus.WARNING); - List disabledLogLevels = logLevels.stream().filter(level -> !enabledLogLevels.contains(level)).collect(Collectors.toList()); - ILog logger = ResourcesPlugin.getPlugin().getLog(); - logLevels.forEach(logLevel -> logger.log(new Status(logLevel, "Some plugin", logLevel.toString()))); - assertSlf4J() - .as("Messages for all enabled levels are logged.").received( - enabledLogLevels.stream().map(i -> i.toString()).collect(Collectors.toList())); - assertSlf4J() - .as("Messages all disabled levels are not logged.").notReceived( - disabledLogLevels.stream().map(i -> i.toString()).collect(Collectors.toList())); - assertEclipse() - .as("All messages received.").received( - logLevels.stream().map(i -> i.toString()).collect(Collectors.toList())); - } - - @Test - void testLogServiceLevel() { - ExtendedLogService service = getService(ExtendedLogService.class); - assertThat(service.isErrorEnabled()).as("Error log level is enabled").isTrue(); - assertThat(service.isWarnEnabled()).as("Warning log level is enabled").isTrue(); - assertThat(service.isInfoEnabled()).as("Info log level is disabled").isFalse(); - assertThat(service.isDebugEnabled()).as("Debug log level is disabled").isFalse(); - assertThat(service.isTraceEnabled()).as("Trace log level is disabled").isFalse(); - } - - @Test - void testLogServiceLog() { - ExtendedLogService service = getService(ExtendedLogService.class); - service.info("Log Info"); - service.warn("Log Warn"); - assertSlf4J().received("Log Warn"); - assertSlf4J().notReceived("Log Info"); - assertEclipse().received(Arrays.asList("Log Warn", "Log Info")); - } - - @Test - @Deprecated - void testLogServiceLog_3_12() { - ExtendedLogService service = getService(ExtendedLogService.class); - service.log(SingleSlf4JService.LOG_INFO, "Log Info"); - try { - throw new IllegalArgumentException(TEST_EXCEPTION_MESSAGE); - } catch (Exception e) { - service.log(SingleSlf4JService.LOG_WARNING, "Log Warn", e); - } - assertSlf4J().received(Arrays.asList("Log Warn", TEST_EXCEPTION_MESSAGE)); - assertSlf4J().notReceived("Log Info"); - assertEclipse().received(Arrays.asList("Log Warn", TEST_EXCEPTION_MESSAGE, "Log Info")); - } - - private static T getService(Class serviceClass) { - ResourcesPlugin plugin = ResourcesPlugin.getPlugin(); - assertThat(plugin).as("ResourcesPlugin instantiated as part of framework defaults").isNotNull(); - Bundle bundle = plugin.getBundle(); - assertThat(bundle).as("ResourcesPlugin has been started.").isNotNull(); - BundleContext context = bundle.getBundleContext(); - assertThat(context).as("ResourcesPlugin has been started.").isNotNull(); - ServiceReference reference = context.getServiceReference(serviceClass); - assertThat(reference).as(serviceClass.getSimpleName() + " has been registered.").isNotNull(); - T service = context.getService(reference); - assertThat(service).as(serviceClass.getSimpleName() + " can be resolved.").isNotNull(); - return service; - } - - private static interface IMessageListener { - Collection getMessages(); - } - - private static class EclipseMessageListener implements LogListener, IMessageListener { - - private final List messages; - - public EclipseMessageListener() { - messages = new ArrayList(); - } - - @Override - public Collection getMessages() { - return Collections.unmodifiableList(messages); - } - - @Override - public void logged(LogEntry entry) { - messages.add(entry.getMessage()); - if (null != entry.getException()) { - messages.add(entry.getException().getMessage()); - } - } - } - - private final static class Slf4JMesssageListener extends PrintStream implements IMessageListener { - - private final List messages; - private final PrintStream originalStream; - - public Slf4JMesssageListener() { - super(System.out); - messages = new ArrayList(); - originalStream = System.out; - System.setOut(this); - } - - @Override - public void println(String x) { - if (x.contains(TEST_LOGGER_NAME)) { - messages.add(x); - } else { - super.println(x); - } - } - - @Override - public void println(Object x) { - if (x instanceof Exception) { - Exception e = (Exception) x; - if (TEST_EXCEPTION_MESSAGE == e.getMessage()) { - messages.add(TEST_EXCEPTION_MESSAGE); - } - } - super.println(x); - } - - public void deregister() { - System.setOut(originalStream); - } - - public void clear() { - messages.clear(); - } - - @Override - public Collection getMessages() { - return Collections.unmodifiableList(messages); - } - } - - private static class MessageListenerAssert extends AbstractAssert, T> { - - public MessageListenerAssert(T actual) { - super(actual, MessageListenerAssert.class); - } - - public ObjectArrayAssert received(Collection unformattedMessages) { - List formattedMessages = unformattedMessages.stream() - .map(unformattedMessage -> getReceivedMessage(unformattedMessage, false)) - .collect(Collectors.toList()); - return new ObjectArrayAssert(formattedMessages.toArray(new String[0])); - } - - public StringAssert received(String unformattedMessage) { - return new StringAssert(getReceivedMessage(unformattedMessage, false)); - } - - public MessageListenerAssert notReceived(String unformattedMessage) { - getReceivedMessage(unformattedMessage, true); - return this; - } - - public MessageListenerAssert notReceived(Collection unformattedMessages) { - unformattedMessages.forEach(unformattedMessage -> getReceivedMessage(unformattedMessage, true)); - return this; - } - - private String getReceivedMessage(String unformattedMessage, boolean negate) { - isNotNull(); - String receivedMessage = null; - for (String formattedMessage : actual.getMessages()) { - if (formattedMessage.contains(unformattedMessage)) { - receivedMessage = formattedMessage; - break; - } - } - if ((!negate) && (null == receivedMessage)) { - failWithMessage("Message <%s> not received.", unformattedMessage); - } - if (negate && (null != receivedMessage)) { - failWithMessage("Message <%s> has been received. Formatted message is: %s", unformattedMessage, receivedMessage); - } - return receivedMessage; - } - - } - - private static MessageListenerAssert assertSlf4J() { - return new MessageListenerAssert(SLF4J_RECEIVER).as("SLF4J"); - } - - private MessageListenerAssert assertEclipse() { - return new MessageListenerAssert(eclipseMessageListener).as("Eclipse"); - } -} diff --git a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSetTest.java b/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSetTest.java deleted file mode 100644 index 9c5c97ddb8..0000000000 --- a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/BundleSetTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.osgi; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Arrays; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleException; - -class BundleSetTest { - - BundleSet instance; - - @BeforeEach - void initialize() { - instance = new BundleSet(); - } - - @Test - void testAddGet() throws BundleException { - Bundle testBundle1 = new TestBundle(1, "a"); - Bundle testBundle2 = new TestBundle(2, "b"); - instance.add(testBundle1); - instance.add(testBundle2); - assertEquals(testBundle1, instance.get(1), "Get by ID 1"); - assertEquals(testBundle2, instance.get(2), "Get by ID 2"); - assertEquals(testBundle1, instance.get("a"), "Get by symbolic name 'a'"); - assertEquals(testBundle2, instance.get("b"), "Get by symbolic name 'b'"); - assertTrue(instance.getAll().containsAll(Arrays.asList(testBundle1, testBundle2)), "Contains all"); - } - - @Test - void testSameSymbolicName() throws BundleException { - final String symbolicName = "sym.a"; - final long id1 = 12345; - final long id2 = 23456; - Bundle testBundle1 = new TestBundle(id1, symbolicName); - Bundle testBundle2 = new TestBundle(id2, symbolicName); - instance.add(testBundle1); - BundleException e = assertThrows(BundleException.class, () -> instance.add(testBundle2)); - assertThat(e.getMessage()).as("BundleException does not contain symbolic name.").contains(symbolicName); - assertThat(e.getMessage()).as("BundleException does not contain ID of exisiting bundle.").contains(Long.toString(id1)); - assertThat(e.getMessage()).as("BundleException does not contain ID of new bundle.").contains(Long.toString(id2)); - } - - @Test - void testSameID() throws BundleException { - final String symbolicName1 = "sym.a"; - final String symbolicName2 = "sym.b"; - final long id = 12345; - Bundle testBundle1 = new TestBundle(id, symbolicName1); - Bundle testBundle2 = new TestBundle(id, symbolicName2); - instance.add(testBundle1); - BundleException e = assertThrows(BundleException.class, () -> instance.add(testBundle2)); - assertThat(e.getMessage()).as("BundleException does not contain ID.").contains(Long.toString(id)); - assertThat(e.getMessage()).as("BundleException does not contain symbolic name of exisiting bundle.").contains(symbolicName1); - assertThat(e.getMessage()).as("BundleException does not contain symbolic name of new bundle.").contains(symbolicName2); - } - -} diff --git a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollectionTest.java b/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollectionTest.java deleted file mode 100644 index 05f18c475c..0000000000 --- a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/ServiceCollectionTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.base.osgi; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashMap; - -import org.assertj.core.api.AbstractAssert; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.osgi.framework.Bundle; -import org.osgi.framework.ServiceException; -import org.osgi.framework.ServiceReference; - -class ServiceCollectionTest { - - ServiceCollection instance; - - @BeforeEach - void initialize() { - Bundle systemBundle = new TestBundle(0, "test.system"); - instance = new ServiceCollection(systemBundle, new HashMap()); - } - - @Test - void testAddGet() { - Service1 service1 = new Service1(); - Service2 service2 = new Service2(); - instance.add(Interf1.class, service1); - instance.add(Interf2a.class, service2); - instance.add(Interf2b.class, service2); - assertFor(instance).getServiceForReferences(Interf1.class).matchesService(service1); - assertFor(instance).getServiceForReferences(Interf2a.class).matchesService(service2); - assertFor(instance).getServiceForReferences(Interf2b.class).matchesService(service2); - } - - @Test - void testMultipleServicesPerInterface() { - Service1 serviceX = new Service1(); - Service1 serviceY = new Service1(); - instance.add(Interf1.class, serviceX); - ServiceException e = assertThrows(ServiceException.class, () -> instance.add(Interf1.class, serviceY)); - assertThat(e.getMessage()).as("ServiceException does not contain interface class name.").contains(Interf1.class.getName()); - } - - private static class ServiceReferenceAssert extends AbstractAssert { - - private final ServiceCollection actual; - private final ServiceReference reference; - - public ServiceReferenceAssert(ServiceCollection actual) { - this(actual, null); - } - - public ServiceReferenceAssert(ServiceCollection actual, ServiceReference reference) { - super(actual, ServiceReferenceAssert.class); - this.reference = reference; - this.actual = actual; - - } - - ServiceReferenceAssert getServiceForReferences(Class interfaceClass) { - ServiceReference[] references = actual.getReferences(interfaceClass.getName()); - int numberOfFoundReferences = null == references ? 0 : references.length; - if (numberOfFoundReferences != 1) { - failWithMessage("Expected to find exactly 1 reference for <%s> , but found %d.", interfaceClass.getName(), numberOfFoundReferences); - } - return new ServiceReferenceAssert(actual, references[0]); - } - - ServiceReferenceAssert matchesService(Object expected) { - if (null == reference) { - failWithMessage("No reference requested."); - } - Object serviceForRef = actual.getService(reference); - if (null == serviceForRef) { - failWithMessage("No service provided for reference."); - } - if (!serviceForRef.equals(expected)) { - failWithMessage("Unexpected service found."); - } - - return this; - } - } - - private static ServiceReferenceAssert assertFor(ServiceCollection actual) { - return new ServiceReferenceAssert(actual); - } - - private static interface Interf1 {}; - - private static interface Interf2a {}; - - private static interface Interf2b {}; - - private static class Service1 implements Interf1 {}; - - private static class Service2 implements Interf2a, Interf2b {}; -} diff --git a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/TestBundle.java b/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/TestBundle.java deleted file mode 100644 index 177442101d..0000000000 --- a/_ext/eclipse-base/src/test/java/com/diffplug/spotless/extra/eclipse/base/osgi/TestBundle.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2016 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.eclipse.base.osgi; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.security.cert.X509Certificate; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.Version; - -/** Helper for testing */ -public class TestBundle implements StaticBundle, TemporaryBundle { - private final String symbolicName; - private final long id; - - public TestBundle(long id, String symbolicName) { - this.id = id; - this.symbolicName = symbolicName; - } - - @Override - public int getState() { - return 0; - } - - @Override - @Deprecated - public Dictionary getHeaders() { - return null; - } - - @Override - public long getBundleId() { - return id; - } - - @Override - public ServiceReference[] getRegisteredServices() { - return null; - } - - @Override - public boolean hasPermission(Object permission) { - return false; - } - - @Override - public URL getResource(String name) { - return null; - } - - @Override - @Deprecated - public Dictionary getHeaders(String locale) { - return null; - } - - @Override - public String getSymbolicName() { - return symbolicName; - } - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - return null; - } - - @Override - public Enumeration getResources(String name) throws IOException { - return null; - } - - @Override - public Enumeration getEntryPaths(String path) { - return null; - } - - @Override - public URL getEntry(String path) { - return null; - } - - @Override - @Deprecated - public Enumeration findEntries(String path, String filePattern, boolean recurse) { - return null; - } - - @Override - public BundleContext getBundleContext() { - return null; - } - - @Override - public Map> getSignerCertificates(int signersType) { - return null; - } - - @Override - public Version getVersion() { - return null; - } - - @Override - public A adapt(Class type) { - return null; - } - - @Override - public File getDataFile(String filename) { - return null; - } - - @Override - public int compareTo(Bundle o) { - return 0; - } - -} diff --git a/_ext/eclipse-cdt/CHANGES.md b/_ext/eclipse-cdt/CHANGES.md deleted file mode 100644 index bdc7d99ea9..0000000000 --- a/_ext/eclipse-cdt/CHANGES.md +++ /dev/null @@ -1,61 +0,0 @@ -# spotless-eclipse-cdt - -We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `9.9.0`). - -## [Unreleased] - -## [10.5.0] - 2021-12-13 -### Added -* Switch to Eclipse CDT release 10.5 for Eclipse 2021-12. - -## [10.4.0] - 2021-09-23 -### Added -* Switch to Eclipse CDT release 10.4 for Eclipse 4.21. - -## [10.3.0] - 2021-06-27 -### Added -* Switch to Eclipse CDT release 10.3 for Eclipse 4.20. - -## [10.2.0] - 2021-06-07 -### Added -* Switch to Eclipse CDT release 10.2 for Eclipse 4.19. - -## [10.1.0] - 2020-12-26 -### Added -* Switch to Eclipse CDT release 10.1 for Eclipse 4.18. - -## [10.0.0] - 2020-10-17 -### Added -* Switch to Eclipse CDT release 10.0 for Eclipse 4.17. -* **BREAKING** Minimum required Java version changed from 8 to 11. - -## [9.11.0] - 2020-10-03 -### Added -* Switch to Eclipse CDT release 9.11.1 for Eclipse 4.16. - -## [9.10.0] - 2020-09-25 -### Added -* Switch to Eclipse CDT release 9.10 for Eclipse 4.14. - -## [9.9.0] - 2019-11-01 -* Switch to Eclipse CDT release 9.9 for Eclipse 4.13 ([#480](https://github.com/diffplug/spotless/issues/480)). - -## [9.8.1] - 2019-10-31 -* Really publish Eclipse CDT release 9.8 for Eclipse 4.12 ([#482](https://github.com/diffplug/spotless/pull/482)). - -## [9.8.0] - 2019-07-24 -* **Known bug** - we actually published Eclipse CDT 9.7 instead of 9.8 - fixed in 9.8.1 -* Switch to Eclipse CDT release 9.8 for Eclipse 4.12 ([#423](https://github.com/diffplug/spotless/pull/423)). - -## [9.7.0] - 2019-03-31 -* Switch to Eclipse CDT release 9.7 for Eclipse 4.11 ([#389](https://github.com/diffplug/spotless/pull/389)). -* Include Eclipse logging allowing formatter warnings/errors to be logged via SLF4J ([#236](https://github.com/diffplug/spotless/issues/236)). - -## [9.4.5] - 2019-02-25 -* Replaced `http` update-site with `https` ([#360](https://github.com/diffplug/spotless/issues/360)). - -## [9.4.4] - 2018-09-04 -* Added missing log service, which caused exceptions on AST warnings ([#286](https://github.com/diffplug/spotless/pull/286)). - -## [9.4.3] - 2018-08-08 -* Initial release! diff --git a/_ext/eclipse-cdt/LICENSE.txt b/_ext/eclipse-cdt/LICENSE.txt deleted file mode 100644 index 3d967aee74..0000000000 --- a/_ext/eclipse-cdt/LICENSE.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. \ No newline at end of file diff --git a/_ext/eclipse-cdt/README.md b/_ext/eclipse-cdt/README.md deleted file mode 100644 index 842fbad606..0000000000 --- a/_ext/eclipse-cdt/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# spotless-eclipse-cdt - -Eclipse CDT is not available in a form which can be easily consumed by maven or gradle. To fix this, we publish Eclipse's formatter and all its dependencies, along with a small amount of glue code, into the `com.diffplug.gradle.spotless:spotless-eclipse-cdt` artifact. - -To publish a new version, update the `_ext/eclipse-cdt/gradle.properties` appropriately and see [CONTRIBUTING.md](../../CONTRIBUTING.md) how to enable -`_ext` projects. - -## License - -Spotless at large is under the Apache 2.0 license, but this jar is under the EPL v1. diff --git a/_ext/eclipse-cdt/build.gradle b/_ext/eclipse-cdt/build.gradle deleted file mode 100644 index 17819f4a2e..0000000000 --- a/_ext/eclipse-cdt/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -ext { - developers = [ - fvgh: [ name: 'Frank Vennemeyer', email: 'frankgh@zoho.com' ], - ] - - p2Repository = "https://download.eclipse.org/tools/cdt/releases/${VER_ECLIPSE_CDT}" - - p2Dependencies = [ - 'org.eclipse.cdt.core':'+', // CodeFormatter and related - ] - -} - -apply from: rootProject.file('_ext/gradle/update-lockfile.gradle') -apply from: rootProject.file('_ext/gradle/p2-fat-jar-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - - -dependencies { - implementation "com.diffplug.spotless:spotless-eclipse-base:${VER_SPOTLESS_ECLISPE_BASE}" - // Provides text partitioners for formatters - implementation ("org.eclipse.platform:org.eclipse.jface.text:${VER_ECLISPE_JFACE}") { - exclude group: 'org.eclipse.platform', module: 'org.eclipse.swt' - } - // Required to by CCorePlugin calling CDTLogWriter - implementation "com.ibm.icu:icu4j:${VER_IBM_ICU}" - // Required to by CCorePlugin calling PositionTrackerManager - implementation "org.eclipse.platform:org.eclipse.core.filebuffers:${VER_ECLISPE_EFS}" - - testImplementation("org.slf4j:slf4j-simple:${VER_SLF4J}") -} - - -////////// -// Test // -////////// -sourceSets { - // Use JAR file with all resources for Eclipse-CDT integration-tests - test.runtimeClasspath = jar.outputs.files + sourceSets.test.output + sourceSets.test.compileClasspath -} diff --git a/_ext/eclipse-cdt/gradle.properties b/_ext/eclipse-cdt/gradle.properties deleted file mode 100644 index 9c1c8a7ee6..0000000000 --- a/_ext/eclipse-cdt/gradle.properties +++ /dev/null @@ -1,12 +0,0 @@ -artifactId=spotless-eclipse-cdt -description=Eclipse's CDT C/C++ formatter bundled for Spotless - -# Build requirements -VER_JAVA=11 - -# Compile dependencies -VER_ECLIPSE_CDT=10.5 -VER_SPOTLESS_ECLISPE_BASE=[3.5.0,4.0.0[ -VER_ECLISPE_JFACE=[3.18.0,4.0.0[ -VER_ECLISPE_EFS=[3.7.0,4.0.0[ -VER_IBM_ICU=[67.1,68[ diff --git a/_ext/eclipse-cdt/src/test/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImplTest.java b/_ext/eclipse-cdt/src/test/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImplTest.java deleted file mode 100644 index 05637a4e22..0000000000 --- a/_ext/eclipse-cdt/src/test/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImplTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.cdt; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; -import java.util.function.Consumer; - -import org.eclipse.cdt.core.CCorePlugin; -import org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants; -import org.junit.jupiter.api.Test; - -/** Eclipse CDT wrapper integration tests */ -class EclipseCdtFormatterStepImplTest { - - private final static String CPP_UNFORMATTED = "#include \n" + - "using namespace std;\n" + - "int main()\n{\n" + - " cout <<\n\"Hello, World!\";\n" + - " return 0;\n" + - "}".replaceAll("\n", LINE_DELIMITER); - private final static String CPP_FORMATTED = "#include \n" + - "using namespace std;\n" + - "int main() {\n" + - "\tcout << \"Hello, World!\";\n" + - "\treturn 0;\n" + - "}\n".replaceAll("\n", LINE_DELIMITER); - - private final static String DOXYGEN_HTML = "/**\n *

void f() {int a =1;} 
\n */\n".replaceAll("\n", LINE_DELIMITER); - - private final static String ILLEGAL_CHAR = Character.toString((char) 254); - - private final static String FUNCT_PTR_UNFORMATTED = "void (*getFunc(void)) (int);"; - private final static String FUNCT_PTR_FORMATTED = "void (* getFunc(void)) (int);"; - - @Test - void defaultFormat() throws Throwable { - String output = format(CPP_UNFORMATTED, config -> {}); - assertEquals(CPP_FORMATTED, - output, "Unexpected formatting with default preferences."); - } - - @Test - void invalidFormat() throws Throwable { - String output = format(CPP_FORMATTED.replace("int main() {", "int main() "), config -> {}); - assertTrue(output.contains("int main()" + LINE_DELIMITER), "Incomplete CPP not formatted on best effort basis."); - } - - @Test - void invalidCharater() throws Throwable { - String output = format(CPP_FORMATTED.replace("int main() {", "int main()" + ILLEGAL_CHAR + " {"), config -> {}); - assertTrue(output.contains("int main()" + LINE_DELIMITER), "Invalid charater not formatted on best effort basis."); - } - - @Test - void invalidConfiguration() throws Throwable { - String output = format(CPP_FORMATTED, config -> { - config.setProperty(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, CCorePlugin.SPACE); - config.setProperty(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, "noInteger"); - }); - assertEquals(CPP_FORMATTED.replace("\t", " "), - output, "Invalid indentation configuration not replaced by default value (4 spaces)"); - } - - @Test - void htmlCommentFormat() throws Throwable { - String output = format(DOXYGEN_HTML + CPP_FORMATTED, config -> {}); - assertEquals(DOXYGEN_HTML + CPP_FORMATTED, - output, "HTML comments not ignored by formatter."); - } - - @Test - void regionWarning() throws Throwable { - String output = format(FUNCT_PTR_UNFORMATTED, config -> {}); - assertEquals(FUNCT_PTR_FORMATTED, output, "Code not formatted at all due to regional error."); - } - - private static String format(final String input, final Consumer config) throws Exception { - Properties properties = new Properties(); - config.accept(properties); - EclipseCdtFormatterStepImpl formatter = new EclipseCdtFormatterStepImpl(properties); - return formatter.format(input); - } -} diff --git a/_ext/eclipse-groovy/CHANGES.md b/_ext/eclipse-groovy/CHANGES.md deleted file mode 100644 index c80ce061d5..0000000000 --- a/_ext/eclipse-groovy/CHANGES.md +++ /dev/null @@ -1,64 +0,0 @@ -# spotless-eclipse-groovy - -We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.5.0`). - -## [Unreleased] - -## [4.3.0] - 2021-10-13 -### Added -* Switch to Groovy-Eclipse release 4.3.0 for Eclipse 4.21 using Groovy 4.0.0 Beta 1. - -## [4.2.0] - 2021-09-14 -### Added -* Switch to Groovy-Eclipse release 4.2.0 for Eclipse 4.20 using Groovy 4.0.0 Alpha 3. - -### Fixed -* Fixed IndexOutOfBoundsException in parallel execution of `eclipse-groovy` formatter ([#877](https://github.com/diffplug/spotless/issues/877)) - -## [4.1.0] - 2021-06-05 -### Added -* Switch to Groovy-Eclipse release 4.1.0 for Eclipse 4.19 using Groovy 4.0.0 Alpha 2. - -## [4.0.0] - 2021-04-10 -### Added -* Switch to Groovy-Eclipse release 4.0.0 for Eclipse 4.18 using Groovy 4.0.0 Alpha 2. -* **BREAKING** Keep spotless-eclipse-groovy major version in sync with Groovy-Eclipse version. - -## [3.9.0] - 2020-10-17 -### Added -* Fixed version number determined by change log. - -## [3.8.1] - 2020-10-17 -### Added -* Switch to Groovy-Eclipse release 3.9.0 for Eclipse 4.17 using Groovy-Indy 3.0.6. - -## [3.8.0] - 2020-10-04 -### Added -* Switch to Groovy-Eclipse release 3.8.0 for Eclipse 4.16 using Groovy-Indy 3.0.4. - -## [3.7.0] - 2020-10-03 -### Added -* Switch to Groovy-Eclipse release 3.7.0 for Eclipse 4.15 using Groovy-Indy 3.0.2. - -## [3.6.0] - 2020-09-26 -### Added -* Switch to Groovy-Eclipse release 3.6.0 for Eclipse 4.14. - -## [3.5.0] - 2019-09-01 -* Switch to Groovy-Eclipse release 3.5.0 for Eclipse 4.13 ([#480](https://github.com/diffplug/spotless/issues/480)). - -## [3.4.0] - 2019-07-24 -* Switch to Groovy-Eclipse release 3.4.0 for Eclipse 4.12 using Groovy-Indy 3.0.0 ([#423](https://github.com/diffplug/spotless/pull/423)). - -## [3.2.0] - 2019-03-16 -* Switch to Groovy-Eclipse release 3.2.0 for Eclipse 4.10 ([#375](https://github.com/diffplug/spotless/pull/375)). -* Include Eclipse logging allowing formatter warnings/errors to be logged via SLF4J ([#236](https://github.com/diffplug/spotless/issues/236)). - -## [3.0.1] - 2019-02-25 -* Replaced `http` update-site with `https` ([#360](https://github.com/diffplug/spotless/issues/360)). - -## [3.0.0] - 2018-09-04 -* Switch to Groovy-Eclipse release 3.0.0 and Groovy 2.6 ([#288](https://github.com/diffplug/spotless/issues/288)). - -## [2.9.2] - 2018-07-19 -* Initial release! diff --git a/_ext/eclipse-groovy/LICENSE.txt b/_ext/eclipse-groovy/LICENSE.txt deleted file mode 100644 index 3d967aee74..0000000000 --- a/_ext/eclipse-groovy/LICENSE.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. \ No newline at end of file diff --git a/_ext/eclipse-groovy/README.md b/_ext/eclipse-groovy/README.md deleted file mode 100644 index c565d04cf2..0000000000 --- a/_ext/eclipse-groovy/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# spotless-eclipse-groovy - -Groovy-Eclipse is not available in a form which can be easily consumed by maven or gradle. -To fix this, we publish Groovy-Eclipse's formatter and all its dependencies, along with a small amount of glue code, into the `com.diffplug.gradle.spotless:spotless-eclipse-groovy` artifact. - -## Build - -To publish a new version, update the `_ext/eclipse-groovy/gradle.properties` appropriately and and see [CONTRIBUTING.md](../../CONTRIBUTING.md) how to enable -`_ext` projects. - -## License - -Spotless at large is under the Apache 2.0 license, but this jar is under the EPL v1. diff --git a/_ext/eclipse-groovy/build.gradle b/_ext/eclipse-groovy/build.gradle deleted file mode 100644 index e1382acd76..0000000000 --- a/_ext/eclipse-groovy/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -ext { - developers = [ - fvgh: [ name: 'Frank Vennemeyer', email: 'frankgh@zoho.com' ], - ] - - p2Repository = "https://dist.springsource.org/release/GRECLIPSE/${VER_GRECLIPSE}/e${VER_ECLIPSE}" - - p2Dependencies = [ - 'org.codehaus.groovy.eclipse.refactoring':'+', // GroovyFormatter and related - - // The following lists does not reflect the complete transitive required packages, but - // the once used during code formatting - 'org.codehaus.groovy':'+', // Groovy compiler patches supporting use within GrEclipse and Groovy itself - 'org.codehaus.groovy.eclipse.core':'+', // Groovy core classes (provides central logging used by formatter) - 'org.eclipse.jdt.core':"${VAR_GRECLIPSE_JDT_PATCH}", // Patches org.eclipse.jdt.core classes supporting use within GrEclipse (provides AST generator) - 'org.eclipse.jdt.groovy.core':'+' // Extends org.eclipse.jdt.core for Groovy - ] - - internalJars = [ - //Jars included by org.codehaus.groovy - ////////////////////////////////////////////////////////////////////////// - // Use Groovy compiler compatible with GrEclipse instead of localGroovy - "**/groovy-${VER_GROOVY}", - "**/groovy-parser2", - // Patches/Overrides some of the Groovy compiler classes - '**/groovy-eclipse', - // Provides logging capabilities for groovy-eclipse - '**/eclipse-trace', - //Jars included by org.eclipse.jdt.groovy.core - ////////////////////////////////////////////////////////////////////////// - //Non locking class loader used by groovy compiler - '**/nlcl' - ] - -} - -apply from: rootProject.file('_ext/gradle/update-lockfile.gradle') -apply from: rootProject.file('_ext/gradle/p2-fat-jar-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -dependencies { - implementation "com.diffplug.spotless:spotless-eclipse-base:${VER_SPOTLESS_ECLISPE_BASE}" - // Provides text partitioners for formatters - implementation ("org.eclipse.platform:org.eclipse.jface.text:${VER_ECLISPE_JFACE}") { - exclude group: 'org.eclipse.platform', module: 'org.eclipse.swt' - } - testImplementation("org.slf4j:slf4j-simple:${VER_SLF4J}") -} - -////////// -// Test // -////////// -sourceSets { - // Use JAR file with all resources for Eclipse-Groovy integration-tests - test.runtimeClasspath = jar.outputs.files + sourceSets.test.output + sourceSets.test.compileClasspath -} diff --git a/_ext/eclipse-groovy/gradle.properties b/_ext/eclipse-groovy/gradle.properties deleted file mode 100644 index b8a21001fd..0000000000 --- a/_ext/eclipse-groovy/gradle.properties +++ /dev/null @@ -1,14 +0,0 @@ -artifactId=spotless-eclipse-groovy -description=Groovy Eclipse's formatter bundled for Spotless - -# Build requirements -VER_JAVA=11 - -# Compile -VER_ECLIPSE=4.21 -VER_SPOTLESS_ECLISPE_BASE=[3.4.2,4.0.0[ -VER_ECLISPE_JFACE=[3.15.300,4.0.0[ -VER_GRECLIPSE=4.3.0 -VER_GROOVY=4.0.0 -# Use org.eclipse.jdt.core patched for Groovy-Eclipse -VAR_GRECLIPSE_JDT_PATCH=3.27.0.v202109301420-e2109-RELEASE diff --git a/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImplTest.java b/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImplTest.java deleted file mode 100644 index 011c68e148..0000000000 --- a/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImplTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.groovy; - -import static com.diffplug.spotless.extra.eclipse.groovy.GrEclipseFormatterStepImpl.IGNORE_FORMATTER_PROBLEMS; -import static org.codehaus.groovy.eclipse.refactoring.PreferenceConstants.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Properties; -import java.util.function.Consumer; - -import org.eclipse.jdt.core.JavaCore; -import org.junit.jupiter.api.Test; - -/** Smoke test checking that transitive dependencies are complete. */ -class GrEclipseFormatterStepImplTest { - - private final static TestData TEST_DATA = TestData.getTestDataOnFileSystem(); - private final static String PARSER_EXCEPTION = "class Test { void method() {} "; - private final static String SCANNER_EXCEPTION = "{"; - private final static String BOUNDED_WILDCARDS_UNFORMATTED = "foo(Map e)\n{\ne.clear();\n}"; - private final static String BOUNDED_WILDCARDS_FORMATTED = "foo(Map e) {\n\te.clear();\n}"; - - @Test - void defaultFormat() throws Throwable { - String output = format(TEST_DATA.input("nominal.test"), config -> {}); - assertEquals(TEST_DATA.expected("nominal.test"), - output, "Unexpected default formatting."); - } - - @Test - void validConfiguration() throws Throwable { - String output = format(TEST_DATA.input("nominal.test"), config -> { - config.put(GROOVY_FORMATTER_REMOVE_UNNECESSARY_SEMICOLONS, "true"); - }); - assertEquals(TEST_DATA.expected("nominal.test").replace(";", ""), - output, "Unexpected formatting for custom configuration."); - } - - @Test - void invalidConfiguration() throws Throwable { - String output = format(TEST_DATA.input("nominal.test"), config -> { - config.put(GROOVY_FORMATTER_INDENTATION, JavaCore.SPACE); - config.put(GROOVY_FORMATTER_INDENTATION_SIZE, "noInteger"); - }); - assertEquals(TEST_DATA.expected("nominal.test").replace("\t", " "), - output, "Groovy formatter does not replace invalid preferences by their defaults."); - } - - /** Test the handling AntlrParserPlugin exceptions by GroovyLogManager.manager logging */ - @Test - void parserException() throws Throwable { - assertThrows(IllegalArgumentException.class, () -> format(PARSER_EXCEPTION, config -> {})); - } - - /** Test the handling GroovyDocumentScanner exceptions by GroovyCore logging */ - @Test - void scannerException() throws Throwable { - assertThrows(IllegalArgumentException.class, () -> format(SCANNER_EXCEPTION, config -> {})); - } - - /** - * Test the handling bounded wildcards templates - * No exception since Groovy-Eclipse 3.0.0. - * Formatting fixed with Groovy-Eclipse 3.14 (org.codehaus.groovy:groovy[3.+]). - */ - @Test - void boundedWildCards() throws Throwable { - String output = format(BOUNDED_WILDCARDS_UNFORMATTED, config -> {}); - assertEquals(BOUNDED_WILDCARDS_FORMATTED, - output, "Unexpected formatting after bounded wildcards."); - } - - @Test - void ignoreCompilerProblems() throws Throwable { - Consumer ignoreCompilerProblems = config -> { - config.setProperty(IGNORE_FORMATTER_PROBLEMS, "true"); - }; - format(PARSER_EXCEPTION, ignoreCompilerProblems); - format(SCANNER_EXCEPTION, ignoreCompilerProblems); - //Test is passed if it does not throw an exception. See issue 237. - } - - private static String format(final String input, final Consumer config) throws Exception { - Properties properties = new Properties(); - config.accept(properties); - GrEclipseFormatterStepImpl formatter = new GrEclipseFormatterStepImpl(properties); - return formatter.format(input); - } - -} diff --git a/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/TestData.java b/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/TestData.java deleted file mode 100644 index b0e4f525da..0000000000 --- a/_ext/eclipse-groovy/src/test/java/com/diffplug/spotless/extra/eclipse/groovy/TestData.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.groovy; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class TestData { - public static TestData getTestDataOnFileSystem() { - final String userDir = System.getProperty("user.dir", "."); - Path dataPath = Paths.get(userDir, "src", "test", "resources"); - if (Files.isDirectory(dataPath)) { - return new TestData(dataPath); - } - return null; - } - - private final Path inputPath; - private final Path expectedPath; - - private TestData(Path dataPath) { - inputPath = dataPath.resolve("input").toAbsolutePath(); - expectedPath = dataPath.resolve("expected").toAbsolutePath(); - for (Path testDataDir : new Path[]{inputPath, expectedPath}) { - if (!Files.isDirectory(testDataDir)) { - throw new IllegalArgumentException(String.format("'%1$s' is not a directory.", testDataDir)); - } - } - } - - public String input(final String fileName) throws Exception { - return read(inputPath.resolve(fileName)); - } - - public String expected(final String fileName) { - Path path = expectedPath.resolve(fileName); - return read(path); - } - - private String read(final Path xmlPath) { - if (!Files.isRegularFile(xmlPath)) { - throw new IllegalArgumentException(String.format("'%1$s' is not a regular file.", xmlPath)); - } - try { - String checkedOutFileContent = new String(java.nio.file.Files.readAllBytes(xmlPath), "UTF8"); - return checkedOutFileContent.replace("\r", ""); //Align GIT end-of-line normalization - } catch (IOException e) { - throw new IllegalArgumentException(String.format("Failed to read '%1$s'.", xmlPath), e); - } - } - -} diff --git a/_ext/eclipse-groovy/src/test/resources/expected/nominal.test b/_ext/eclipse-groovy/src/test/resources/expected/nominal.test deleted file mode 100644 index a40d247bad..0000000000 --- a/_ext/eclipse-groovy/src/test/resources/expected/nominal.test +++ /dev/null @@ -1,13 +0,0 @@ -class TestClass { - def a; - - TestClass(String s) { - this.a = s - } - def methodNamedArgs(Map args) { - "named args: $args" - } -} - -def t = new TestClass() -def arr = [4, 'string1', 'string2'] diff --git a/_ext/eclipse-groovy/src/test/resources/input/nominal.test b/_ext/eclipse-groovy/src/test/resources/input/nominal.test deleted file mode 100644 index 85f742677f..0000000000 --- a/_ext/eclipse-groovy/src/test/resources/input/nominal.test +++ /dev/null @@ -1,15 +0,0 @@ -class TestClass { - def a; - -TestClass(String s) { -this.a = s -} - def methodNamedArgs(Map args) { - "named args: $args" - } -} - -def t = new TestClass() -def arr = [4, -'string1', -'string2'] diff --git a/_ext/eclipse-jdt/CHANGES.md b/_ext/eclipse-jdt/CHANGES.md deleted file mode 100644 index d698ce2c6c..0000000000 --- a/_ext/eclipse-jdt/CHANGES.md +++ /dev/null @@ -1,15 +0,0 @@ -# spotless-eclipse-jdt - -We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `4.8.0`). - -## [Unreleased] - -## [4.8.1] - 2021-10-04 -### Changed -* Bumped minimum supported Eclipse JDT core version to 3.27.0 -* `format` interface requires source file information to distinguish module-info from compilation unit. Old interface marked as deprecated. -### Fixed -* Fixed module-info formatting. Previous versions did not recognized content and skipped formatting. - -## [4.8.0] - 2018-07-19 -* Initial release! diff --git a/_ext/eclipse-jdt/LICENSE.txt b/_ext/eclipse-jdt/LICENSE.txt deleted file mode 100644 index 3d967aee74..0000000000 --- a/_ext/eclipse-jdt/LICENSE.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. \ No newline at end of file diff --git a/_ext/eclipse-jdt/README.md b/_ext/eclipse-jdt/README.md deleted file mode 100644 index 014b58e732..0000000000 --- a/_ext/eclipse-jdt/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# spotless-eclipse-jdt - -Eclipse JDT and its dependencies require a large amount of byte code. -Hence they should not be directly be required by the Spotless, but only be requested in case -they are configured by the Spotless configuration. Hence we publish Eclipse's formatter and all its dependencies, along with a small amount of glue code, into the `com.diffplug.gradle.spotless:spotless-eclipse-jdt` artifact. - -To publish a new version, update the `_ext/eclipse-jdt/gradle.properties` appropriately and and see [CONTRIBUTING.md](../../CONTRIBUTING.md) how to enable -`_ext` projects. - -## License - -Spotless at large is under the Apache 2.0 license, but this jar is under the EPL v1. diff --git a/_ext/eclipse-jdt/build.gradle b/_ext/eclipse-jdt/build.gradle deleted file mode 100644 index b0967822a9..0000000000 --- a/_ext/eclipse-jdt/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply from: rootProject.file('_ext/gradle/update-lockfile.gradle') -apply from: rootProject.file('_ext/gradle/java-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -ext { - developers = [ - fvgh: [ name: 'Frank Vennemeyer', email: 'frankgh@zoho.com' ], - nedtwigg: [ name: 'Ned Twigg', email: 'ned.twigg@diffplug.com' ], - ] -} - -dependencies { - implementation "com.diffplug.spotless:spotless-eclipse-base:${VER_SPOTLESS_ECLISPE_BASE}" - implementation("org.eclipse.jdt:org.eclipse.jdt.core:${VER_ECLIPSE_JDT_CORE}") { - exclude group: 'org.eclipse.platform', module: 'org.eclipse.ant.core' - exclude group: 'org.eclipse.platform', module: 'org.eclipse.core.expressions' - exclude group: 'org.eclipse.platform', module: 'org.eclipse.core.filesystem' - } -} \ No newline at end of file diff --git a/_ext/eclipse-jdt/gradle.properties b/_ext/eclipse-jdt/gradle.properties deleted file mode 100644 index 82a4423140..0000000000 --- a/_ext/eclipse-jdt/gradle.properties +++ /dev/null @@ -1,9 +0,0 @@ -artifactId=spotless-eclipse-jdt -description=Eclipse's JDT formatter bundled for Spotless - -# Build requirements -VER_JAVA=11 - -# Compile -VER_ECLIPSE_JDT_CORE=[3.13.0,4.0.0[ -VER_SPOTLESS_ECLISPE_BASE=[3.5.0,4.0.0[ diff --git a/_ext/eclipse-jdt/src/test/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImplTest.java b/_ext/eclipse-jdt/src/test/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImplTest.java deleted file mode 100644 index cef63072c9..0000000000 --- a/_ext/eclipse-jdt/src/test/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImplTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.java; - -import static org.junit.jupiter.api.Assertions.*; - -import java.io.File; -import java.io.IOException; -import java.util.Properties; - -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.diffplug.spotless.FormatterProperties; -import com.diffplug.spotless.ResourceHarness; - -/** Eclipse JDT wrapper integration tests */ -class EclipseJdtFormatterStepImplTest extends ResourceHarness { - - private static String UNFORMATTED; - private static String FORMATTED; - - @BeforeAll - static void beforeAll() throws IOException { - UNFORMATTED = getTestResource("java/eclipse/JavaCodeUnformatted.test"); - FORMATTED = getTestResource("java/eclipse/JavaCodeFormatted.test"); - } - - private Properties config; - - @BeforeEach - void beforeEach() throws IOException { - File settingsFile = createTestFile("java/eclipse/formatter.xml"); - config = FormatterProperties.from(settingsFile).getProperties(); - } - - private final static String ILLEGAL_CHAR = Character.toString((char) 254); - - @Test - void nominal() throws Throwable { - assertEquals(FORMATTED, format(UNFORMATTED)); - } - - @Test - public void invalidSyntax() throws Throwable { - try { - String invalidSyntax = FORMATTED.replace("{", ""); - assertEquals(invalidSyntax, format(invalidSyntax)); - } catch (IndexOutOfBoundsException e) { - /* - * Some JDT versions throw exception, but this changed again in later versions. - * Anyhow, exceptions are acceptable, since Spotless should format valid Java code. - */ - } - } - - @Test - void invalidCharater() throws Throwable { - String invalidInput = UNFORMATTED + ILLEGAL_CHAR; - assertEquals(invalidInput, format(invalidInput)); - } - - @Test - void invalidConfiguration() throws Throwable { - config.setProperty(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaCore.SPACE); - config.setProperty(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, "noInteger"); - String defaultTabReplacement = " "; - assertEquals(FORMATTED.replace("\t", defaultTabReplacement), format(FORMATTED)); - } - - @Test - void htmlPreTag() throws Throwable { - config.clear(); - config.setProperty( - DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_HEADER, - DefaultCodeFormatterConstants.TRUE); - String formatted = getTestResource("java/eclipse/HtmlPreTagFormatted.test"); - String unformatted = getTestResource("java/eclipse/HtmlPreTagUnformatted.test"); - assertEquals(formatted, format(unformatted), "Failed to create internal code formatter. See Spotless issue #191"); - } - - @Test - void moduleInfo() throws Throwable { - File settingsFile = createTestFile("java/eclipse/ModuleInfo.prefs"); - config = FormatterProperties.from(settingsFile).getProperties(); - String formatted = getTestResource("java/eclipse/ModuleInfoFormatted.test"); - String unformatted = getTestResource("java/eclipse/ModuleInfoUnformatted.test"); - assertEquals(formatted, format(unformatted, "whatever/module-info.java"), "Jvm9 module info not formatted."); - } - - private String format(final String input) throws Exception { - return format(input, ""); - } - - private String format(final String input, final String fileName) throws Exception { - EclipseJdtFormatterStepImpl formatter = new EclipseJdtFormatterStepImpl(config); - return formatter.format(input, new File(fileName)); - } - -} diff --git a/_ext/eclipse-wtp/CHANGES.md b/_ext/eclipse-wtp/CHANGES.md deleted file mode 100644 index 6fc919aa0e..0000000000 --- a/_ext/eclipse-wtp/CHANGES.md +++ /dev/null @@ -1,74 +0,0 @@ -# spotless-eclipse-wtp - -We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.15.1`). - -## [Unreleased] - -## [3.23.0] - 2021-09-22 -### Added -* Switch to Web Tools Platform release 3.23.0 for Eclipse 4.21. - -## [3.22.0] - 2021-06-27 -### Added -* Switch to Web Tools Platform release 3.22.0 for Eclipse 4.20. - -## [3.21.0] - 2021-06-07 -### Added -* Switch to Web Tools Platform release 3.21.0 for Eclipse 4.19. Minimum required Java version changed from 8 to 11. - -## [3.20.0] - 2020-12-26 -### Added -* Switch to Web Tools Platform release 3.20.0 for Eclipse 4.18. - -## [3.19.0] - 2020-10-17 -### Added -* Fixed version number determined by change log. - -## [3.18.1] - 2020-10-17 -### Added -* Switch to Web Tools Platform release 3.19.0 for Eclipse 4.17. - -## [3.18.0] - 2020-10-03 -### Added -* Switch to Web Tools Platform release 3.18.0 for Eclipse 4.16. - -## [3.17.0] - 2020-10-03 -### Added -* Switch to Web Tools Platform release 3.17.0 for Eclipse 4.15. - -## [3.16.0] - 2020-09-26 -### Added -* Switch to Web Tools Platform release 3.16.0 for Eclipse 4.14. - -## [3.15.3] - 2020-03-26 -### Fixed -* Handling of character encodings on OS with non-unicode default file encoding format. CSS, HTML and JSON formatter steps encoded intermediately the input using the default file encoding, which was not lossless if the default file encoding did not support the full unicode character set. ([#545](https://github.com/diffplug/spotless/issues/545)). - -## [3.15.2] - 2020-03-04 -### Fixed -* Racing conditions in WTP formatter configurations. Multiple configurations within the same project are no longer supported. ([#492](https://github.com/diffplug/spotless/pull/492)). - -## [3.15.1] - 2019-11-27 -* Bugfix: Fix NPE in EclipseXmlFormatterStepImpl ([#490](https://github.com/diffplug/spotless/pull/490)). - -## [3.15.0] - 2019-11-06 -* Switch to Web Tools Platform release 3.15.0 for Eclipse 4.13 ([#480](https://github.com/diffplug/spotless/issues/480)). - -## [3.14.0] - 2019-06-24 -* Switch to Web Tools Platform release 3.14.0 for Eclipse 4.12 ([#423](https://github.com/diffplug/spotless/pull/423)). - -## [3.10.0] - 2019-03-17 -* Switch to Web Tools Platform release 3.10.0 for Eclipse 4.8 ([#378](https://github.com/diffplug/spotless/pull/378)). -* Include Eclipse logging allowing formatter warnings/errors to be logged via SLF4J ([#236](https://github.com/diffplug/spotless/issues/236)). - -## [3.9.8] - 2019-03-10 -* XML formatter ignores external URIs per default. ([#369](https://github.com/diffplug/spotless/issues/369)). Add `resolveExternalURI=true` property to switch to previous behavior. - -## [3.9.7] - 2019-02-25 -* Replaced `http` update-site with `https` ([#360](https://github.com/diffplug/spotless/issues/360)). - -## [3.9.6] - 2019-02-11 -* Fixed formatting of JSON arrays ([#344](https://github.com/diffplug/spotless/issues/344)). - -## [3.9.5] - 2018-08-08 -* Initial release! diff --git a/_ext/eclipse-wtp/LICENSE.txt b/_ext/eclipse-wtp/LICENSE.txt deleted file mode 100644 index 3d967aee74..0000000000 --- a/_ext/eclipse-wtp/LICENSE.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. \ No newline at end of file diff --git a/_ext/eclipse-wtp/README.md b/_ext/eclipse-wtp/README.md deleted file mode 100644 index 879f299377..0000000000 --- a/_ext/eclipse-wtp/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# spotless-eclipse-wtp - -Eclipse WTP is not available in a form which can be easily consumed by maven or gradle. To fix this, we publish Eclipse's WTP formatters, along with a small amount of glue code, into the `com.diffplug.spotless.extra:spotless-eclipse-wtp` artifact. - -To publish a new version, update the `_ext/eclipse-wtp/gradle.properties` appropriately and and see [CONTRIBUTING.md](../../CONTRIBUTING.md) how to enable -`_ext` projects. - -## License - -Spotless at large is under the Apache 2.0 license, but this jar is under the EPL v1. diff --git a/_ext/eclipse-wtp/build.gradle b/_ext/eclipse-wtp/build.gradle deleted file mode 100644 index 4001e100c2..0000000000 --- a/_ext/eclipse-wtp/build.gradle +++ /dev/null @@ -1,126 +0,0 @@ -ext { - developers = [ - fvgh: [ name: 'Frank Vennemeyer', email: 'frankgh@zoho.com' ], - ] - - p2Repository = "https://download.eclipse.org/webtools/repository/${VER_ECLIPSE_WTP}" - - p2Dependencies = [ - // XML/HTML Formatter - Dependencies - 'org.eclipse.wst.xml.core':'+', // DefaultXMLPartitionFormatter and XMLAssociationProvider - 'org.eclipse.wst.sse.core':'+', // Structure models - 'org.eclipse.wst.common.uriresolver':'+', // URI resolver for model queries - 'org.eclipse.wst.dtd.core':'+', // Support DTD extensions - - // XML Formatter - Dependencies - 'org.eclipse.wst.xsd.core':'+', // Support XSD extensions - - // JS Formatter - Dependencies - 'org.eclipse.wst.jsdt.core':'+', // DefaultCodeFormatter and related - 'org.eclipse.wst.jsdt.ui':'+', // Functionality to format comments - - // JSON Formatter - Dependencies - 'org.eclipse.wst.json.core':'+', // FormatProcessorJSON and related - 'org.eclipse.json':'+', // Provides JSON node interfaces - - // CSS Formatter - Dependencies - 'org.eclipse.wst.css.core':'+', // FormatProcessorCSS and related - - // HTML Formatter - Dependencies - 'org.eclipse.wst.html.core':'+', // HTMLFormatProcessorImpl and related - ] - - jarInclude = [ - '**/*.class', - // Take all classes - '**/*.properties', - // Text resources (for messages, etc) - '**/*.rsc', - // JSDT requires gramar files - '**/*.xml', - // Plugin XML and other resources - '*.html', - // License information about the included JARs, - 'META-INF/**' // Plugin manifest and addtional information - ] - - fatJarResourcesMap = [ - 'org.eclipse.wst.common.uriresolver': 'org.eclipse.wst.common.uriresolver.internal.provisional', - 'org.eclipse.wst.css.core': 'org.eclipse.wst.css.core.internal', - 'org.eclipse.wst.dtd.core': 'org.eclipse.wst.dtd.core.internal', - 'org.eclipse.wst.html.core': 'org.eclipse.wst.html.core.internal', - 'org.eclipse.wst.sse.core': 'org.eclipse.wst.sse.core.internal.encoding.util', - 'org.eclipse.wst.xml.core': 'org.eclipse.wst.xml.core.internal', - 'org.eclipse.wst.xsd.core': 'org.eclipse.wst.xsd.core.internal' - ] -} - -apply from: rootProject.file('_ext/gradle/update-lockfile.gradle') -apply from: rootProject.file('_ext/gradle/p2-fat-jar-setup.gradle') -apply from: rootProject.file('gradle/java-publish.gradle') - -dependencies { - implementation "com.diffplug.spotless:spotless-eclipse-base:${VER_SPOTLESS_ECLISPE_BASE}" - // Required by most WPT formatters - implementation "com.ibm.icu:icu4j:${VER_IBM_ICU}" - // The XSD/DTD and other models are defined with EMF. - implementation "org.eclipse.emf:org.eclipse.emf.common:${VER_ECLISPE_EMF}" - implementation "org.eclipse.emf:org.eclipse.emf.ecore:${VER_ECLISPE_EMF}" - // Some WPT plugins requires OSGI bundle interfaces (but not effectively used) - implementation "org.eclipse.platform:org.eclipse.osgi.services:${VER_ECLIPSE_OSGI_SERVICES}" - // Provides document data structure and file buffers for formatters - implementation "org.eclipse.platform:org.eclipse.core.filebuffers:${VER_ECLIPSE_FILE_BUFFERS}" - // Provides text partitioners for formatters - implementation ("org.eclipse.platform:org.eclipse.jface.text:${VER_ECLISPE_JFACE}") { - exclude group: 'org.eclipse.platform', module: 'org.eclipse.swt' - } - // Some WPT plugins use the EFS for storing temporary worspace data - implementation "org.eclipse.platform:org.eclipse.core.filesystem:${VER_ECLISPE_EFS}" - // Required by org.eclipse.wst.xsd.core - implementation "org.eclipse.emf:org.eclipse.xsd:${VER_ECLISPE_XSD}" - - testImplementation("org.slf4j:slf4j-simple:${VER_SLF4J}") -} - -jar { - manifest { - from 'src/main/resources/META-INF/MANIFEST.MF' - } -} - -////////// -// Test // -////////// -sourceSets { - // Use JAR file with all resources for Eclipse-WTP integration-tests - test.runtimeClasspath = jar.outputs.files + sourceSets.test.output + sourceSets.test.compileClasspath -} - -/* - * All test classes need to run separately since they all instatiate different setups of the - * Eclipse framework. - */ -test { - //Skip default tests, which would run every test case. - exclude '**' -} - -//Instead make a separate test task per case -def testLocation = 'src/test/java' -fileTree(dir: testLocation).include('**/*Test.java').each { file -> - def testFile = file.getName().replace(".java", "") - def filePath = file.getAbsolutePath().replace(".java", "**") //Don't ask me why the task is not happy when it gets no asterisk - filePath = filePath.substring(filePath.lastIndexOf(testLocation) + testLocation.length() + 1) - task "${testFile}"(type: Test) { - group = LifecycleBasePlugin.VERIFICATION_GROUP - description = "Runs ${testFile} integration test." - include "${filePath}" - reports { - html.destination = new File("$buildDir/reports/${testFile}") - junitXml.destination = new File("$buildDir/${testFile}") - } - //classpath = jar.outputs.files + sourceSets.test.output + sourceSets.test.compileClasspath - mustRunAfter tasks.jar - } - test.dependsOn "${testFile}" -} diff --git a/_ext/eclipse-wtp/gradle.properties b/_ext/eclipse-wtp/gradle.properties deleted file mode 100644 index 743748d1ba..0000000000 --- a/_ext/eclipse-wtp/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -artifactId=spotless-eclipse-wtp -description=Eclipse's WTP formatters bundled for Spotless - -# Build requirements -VER_JAVA=11 - -# Compile -VER_ECLIPSE_WTP=2021-09 -VER_SPOTLESS_ECLISPE_BASE=[3.5.0,4.0.0[ -VER_IBM_ICU=[67.1,68[ -VER_ECLISPE_EMF=[2.22.0,3.0.0[ -VER_ECLIPSE_OSGI_SERVICES=[3.10.0,4.0.0[ -VER_ECLIPSE_FILE_BUFFERS=[3.7.0,4.0.0[ -VER_ECLISPE_JFACE=[3.18.0,4.0.0[ -VER_ECLISPE_EFS=[1.9.0,2.0.0[ -VER_ECLISPE_XSD=[2.18.0,3.0.0[ diff --git a/_ext/eclipse-wtp/spotless.xmlformat.prefs b/_ext/eclipse-wtp/spotless.xmlformat.prefs deleted file mode 100644 index 3bbd7ecd72..0000000000 --- a/_ext/eclipse-wtp/spotless.xmlformat.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -indentationChar=space -indentationSize=2 -lineWidth=999 diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImpl.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImpl.java deleted file mode 100644 index fb0e1ffeac..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import java.util.Properties; - -import org.eclipse.wst.css.core.internal.CSSCorePlugin; -import org.eclipse.wst.css.core.internal.cleanup.CleanupProcessorCSS; -import org.eclipse.wst.css.core.internal.preferences.CSSCorePreferenceInitializer; -import org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.wtp.sse.CleanupStep; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** Formatter step which calls out to the Eclipse CSS cleanup and formatter. */ -public class EclipseCssFormatterStepImpl extends CleanupStep { - - public EclipseCssFormatterStepImpl(Properties properties) throws Exception { - super(new CleanupProcessor(), new FrameworkConfig(properties)); - PluginPreferences.assertNoChanges(CSSCorePlugin.getDefault(), properties); - } - - /** - * The FormatProcessorCSS does not allow a strict case formatting. - * Hence additionally the CleanupProcessorCSS is used. - */ - private static class CleanupProcessor extends CleanupProcessorCSS implements CleanupStep.ProcessorAccessor { - @Override - public String getTypeId() { - return getContentType(); - } - - @Override - public void refreshPreferences() { - refreshCleanupPreferences(); - } - - @Override - public AbstractStructuredCleanupProcessor get() { - return this; - } - } - - static class FrameworkConfig extends CleanupStep.FrameworkConfig { - private final Properties properties; - - FrameworkConfig(Properties properties) { - this.properties = properties; - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - super.activatePlugins(config); - activateCssPlugins(config); - } - - static void activateCssPlugins(SpotlessEclipsePluginConfig config) { - config.add(new CSSCorePlugin()); - } - - @Override - public void customize() { - PluginPreferences.configure(CSSCorePlugin.getDefault(), new CSSCorePreferenceInitializer(), properties); - } - } -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImpl.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImpl.java deleted file mode 100644 index 4b7f592c3c..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImpl.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; - -import org.eclipse.wst.html.core.internal.HTMLCorePlugin; -import org.eclipse.wst.html.core.internal.cleanup.HTMLCleanupProcessorImpl; -import org.eclipse.wst.html.core.internal.encoding.HTMLDocumentLoader; -import org.eclipse.wst.html.core.internal.format.HTMLFormatProcessorImpl; -import org.eclipse.wst.html.core.internal.preferences.HTMLCorePreferenceInitializer; -import org.eclipse.wst.html.core.text.IHTMLPartitions; -import org.eclipse.wst.jsdt.core.JavaScriptCore; -import org.eclipse.wst.jsdt.core.ToolFactory; -import org.eclipse.wst.jsdt.core.formatter.CodeFormatter; -import org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor; -import org.eclipse.wst.sse.core.internal.format.IStructuredFormatProcessor; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseCoreConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.wtp.html.JsRegionProcessor; -import com.diffplug.spotless.extra.eclipse.wtp.html.StructuredDocumentProcessor; -import com.diffplug.spotless.extra.eclipse.wtp.sse.CleanupStep; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** Formatter step which calls out to the Eclipse HTML cleanup and formatter. */ -public class EclipseHtmlFormatterStepImpl extends CleanupStep { - - private final String htmlFormatterIndent; - private final CodeFormatter jsFormatter; - - public EclipseHtmlFormatterStepImpl(Properties properties) throws Exception { - super(new CleanupProcessor(), new FrameworkConfig(properties)); - PluginPreferences.assertNoChanges(HTMLCorePlugin.getDefault(), properties); - htmlFormatterIndent = ((CleanupProcessor) processorAccessor).getIndent(); - jsFormatter = ToolFactory.createCodeFormatter(JavaScriptCore.getOptions(), ToolFactory.M_FORMAT_EXISTING); - } - - @Override - public String format(String raw) throws Exception { - raw = super.format(raw); - - // Not sure how Eclipse binds the JS formatter to HTML. The formatting is accomplished manually instead. - IStructuredDocument document = (IStructuredDocument) new HTMLDocumentLoader().createNewStructuredDocument(); - document.setPreferredLineDelimiter(LINE_DELIMITER); - document.set(raw); - StructuredDocumentProcessor jsProcessor = new StructuredDocumentProcessor( - document, IHTMLPartitions.SCRIPT, JsRegionProcessor.createFactory(htmlFormatterIndent)); - jsProcessor.apply(jsFormatter); - - return document.get(); - } - - /** - * * The WTP {@link HTMLFormatProcessorImpl} does not allow a strict case formatting. - * Hence additionally the {@link HTMLCleanupProcessorImpl} is used. - *

- * Note that a preferences like {@code TAG_NAME_CASE} are not used by the - * formatter, though configurable in the formatters preference GUI. - * The user must instead configure for example {@code CLEANUP_TAG_NAME_CASE} - * in the cleanup GUI. - *

- */ - private static class CleanupProcessor extends HTMLCleanupProcessorImpl implements CleanupStep.ProcessorAccessor { - private HTMLFormatProcessorImpl processor; - - CleanupProcessor() { - processor = new HTMLFormatProcessorImpl(); - } - - @Override - public String getTypeId() { - return getContentType(); - } - - @Override - public void refreshPreferences() { - refreshCleanupPreferences(); - processor = new HTMLFormatProcessorImpl(); //Constructor reads new preferences - processor.refreshFormatPreferences = false; //Don't refresh when cloning - } - - @Override - public AbstractStructuredCleanupProcessor get() { - return this; - } - - @Override - protected IStructuredFormatProcessor getFormatProcessor() { - return processor; - } - - String getIndent() { - return processor.getFormatPreferences().getIndent(); - } - - } - - private static class FrameworkConfig extends CleanupStep.FrameworkConfig { - private final List dependentConfigs; - private final Properties properties; - - public FrameworkConfig(Properties properties) { - dependentConfigs = Arrays.asList( - new EclipseCssFormatterStepImpl.FrameworkConfig(properties), - new EclipseJsFormatterStepImpl.FrameworkConfig(properties), - new EclipseXmlFormatterStepImpl.FrameworkConfig(properties)); - this.properties = properties; - } - - @Override - public void registerBundles(SpotlessEclipseCoreConfig config) { - EclipseJsFormatterStepImpl.FrameworkConfig.registerNonHeadlessBundles(config); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - super.activatePlugins(config); - EclipseCssFormatterStepImpl.FrameworkConfig.activateCssPlugins(config); - EclipseJsFormatterStepImpl.FrameworkConfig.activateJsPlugins(config); - /* - * The HTML formatter only uses the DOCTYPE/SCHEMA for content model selection. - * Hence no external URIs are required. - */ - boolean allowExternalURI = false; - EclipseXmlFormatterStepImpl.FrameworkConfig.activateXmlPlugins(config, allowExternalURI); - config.add(new HTMLCorePlugin()); - } - - @Override - public void customize() { - dependentConfigs.stream().forEach(c -> { - c.customize(); - }); - PluginPreferences.configure(HTMLCorePlugin.getDefault(), new HTMLCorePreferenceInitializer(), properties); - } - - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImpl.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImpl.java deleted file mode 100644 index c101b1a27a..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImpl.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.DefaultBundles.*; -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; - -import java.util.AbstractMap.SimpleEntry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.Document; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IDocumentPartitioner; -import org.eclipse.jface.text.ITypedRegion; -import org.eclipse.jface.text.TextUtilities; -import org.eclipse.jface.text.TypedPosition; -import org.eclipse.jface.text.formatter.FormattingContextProperties; -import org.eclipse.jface.text.formatter.IFormattingContext; -import org.eclipse.jface.text.rules.FastPartitioner; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.jsdt.core.JavaScriptCore; -import org.eclipse.wst.jsdt.core.ToolFactory; -import org.eclipse.wst.jsdt.core.formatter.CodeFormatter; -import org.eclipse.wst.jsdt.core.formatter.DefaultCodeFormatterConstants; -import org.eclipse.wst.jsdt.internal.ui.text.FastJavaPartitionScanner; -import org.eclipse.wst.jsdt.internal.ui.text.comment.CommentFormattingContext; -import org.eclipse.wst.jsdt.internal.ui.text.comment.CommentFormattingStrategy; -import org.eclipse.wst.jsdt.ui.text.IJavaScriptPartitions; -import org.osgi.framework.Bundle; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseCoreConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** Formatter step which calls out to the Eclipse JS formatter. */ -public class EclipseJsFormatterStepImpl { - - private final static String[] COMMENT_TYPES = { - IJavaScriptPartitions.JAVA_DOC, - IJavaScriptPartitions.JAVA_MULTI_LINE_COMMENT, - IJavaScriptPartitions.JAVA_SINGLE_LINE_COMMENT, - IJavaScriptPartitions.JAVA_STRING, - IJavaScriptPartitions.JAVA_CHARACTER - }; - - private final static Map OPTION_2_COMMENT_TYPE = Collections.unmodifiableMap(Stream.of( - new SimpleEntry<>(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_LINE_COMMENT, IJavaScriptPartitions.JAVA_SINGLE_LINE_COMMENT), - new SimpleEntry<>(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_BLOCK_COMMENT, IJavaScriptPartitions.JAVA_MULTI_LINE_COMMENT), - new SimpleEntry<>(DefaultCodeFormatterConstants.FORMATTER_COMMENT_FORMAT_JAVADOC_COMMENT, IJavaScriptPartitions.JAVA_DOC)).collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue()))); - - private final CodeFormatter formatter; - private final Hashtable options; - private final Set commentTypesToBeFormatted; - - public EclipseJsFormatterStepImpl(Properties properties) throws Exception { - SpotlessEclipseFramework.setup(new FrameworkConfig(properties)); - PluginPreferences.assertNoChanges(JavaScriptCore.getPlugin(), properties); - options = JavaScriptCore.getOptions(); - commentTypesToBeFormatted = OPTION_2_COMMENT_TYPE.entrySet().stream().filter(x -> DefaultCodeFormatterConstants.TRUE.equals(options.get(x.getKey()))).map(x -> x.getValue()).collect(Collectors.toSet()); - formatter = ToolFactory.createCodeFormatter(options, ToolFactory.M_FORMAT_EXISTING); - } - - /** Formatting JavaScript string */ - public String format(String raw) throws Exception { - raw = formatComments(raw); - // The comment formatter messed up the code a little bit (adding some line breaks). Now we format the code. - IDocument doc = new Document(raw); - TextEdit edit = formatter.format(CodeFormatter.K_JAVASCRIPT_UNIT, raw, 0, raw.length(), 0, LINE_DELIMITER); - if (edit == null) { - throw new IllegalArgumentException("Invalid JavaScript syntax for formatting."); - } else { - edit.apply(doc); - } - return doc.get(); - } - - /** - * Comment formats like it would be accomplished by the JDTS UI, without setting up the UI. - * @see org.eclipse.wst.jsdt.internal.ui.fix.CommentFormatFix - */ - private String formatComments(String raw) { - Document doc = new Document(raw); - IDocumentPartitioner commentPartitioner = new FastPartitioner(new FastJavaPartitionScanner(), COMMENT_TYPES); - doc.setDocumentPartitioner(IJavaScriptPartitions.JAVA_PARTITIONING, commentPartitioner); - commentPartitioner.connect(doc); - CommentFormattingStrategy commentFormatter = new CommentFormattingStrategy(); - IFormattingContext context = new CommentFormattingContext(); - context.setProperty(FormattingContextProperties.CONTEXT_PREFERENCES, options); - context.setProperty(FormattingContextProperties.CONTEXT_DOCUMENT, Boolean.TRUE); - context.setProperty(FormattingContextProperties.CONTEXT_MEDIUM, doc); - try { - ITypedRegion[] regions = TextUtilities.computePartitioning(doc, IJavaScriptPartitions.JAVA_PARTITIONING, 0, doc.getLength(), false); - MultiTextEdit resultEdit = new MultiTextEdit(); - Arrays.asList(regions).stream().filter(reg -> commentTypesToBeFormatted.contains(reg.getType())).forEach(region -> { - TypedPosition typedPosition = new TypedPosition(region.getOffset(), region.getLength(), region.getType()); - context.setProperty(FormattingContextProperties.CONTEXT_PARTITION, typedPosition); - commentFormatter.formatterStarts(context); - TextEdit edit = commentFormatter.calculateTextEdit(); - commentFormatter.formatterStops(); - if (null != edit && edit.hasChildren()) { - resultEdit.addChild(edit); - } - }); - resultEdit.apply(doc); - return doc.get(); - } catch (BadLocationException e) { - //Silently ignore comment formatting exceptions and return the original string - return raw; - } - } - - static class FrameworkConfig implements SpotlessEclipseConfig { - private final Properties properties; - - FrameworkConfig(Properties properties) { - this.properties = properties; - } - - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(this.getClass().getPackage().getName()); - } - - @Override - public void registerBundles(SpotlessEclipseCoreConfig config) { - registerNonHeadlessBundles(config); - } - - static void registerNonHeadlessBundles(SpotlessEclipseCoreConfig config) { - //The JS model requires the JDT indexer, hence a headless Eclipse cannot be used. - config.add(PLATFORM, Bundle.ACTIVE); - config.add(REGISTRY, PREFERENCES, COMMON); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - activateJsPlugins(config); - } - - static void activateJsPlugins(SpotlessEclipsePluginConfig config) { - // The JS core uses EFS for determination of temporary storage location - config.add(org.eclipse.core.filesystem.EFS.class); - // The JS core provides the JSDT formatter - config.add(new org.eclipse.wst.jsdt.core.JavaScriptCore()); - } - - @Override - @SuppressWarnings("unchecked") - public void customize() { - PluginPreferences.store(JavaScriptCore.getPlugin(), properties); - Hashtable options = JavaScriptCore.getDefaultOptions(); - options.putAll(DefaultCodeFormatterConstants.getJSLintConventionsSettings()); - options.putAll(new HashMap(properties)); - JavaScriptCore.setOptions(options); - } - - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImpl.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImpl.java deleted file mode 100644 index 91d8516b24..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImpl.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import java.io.IOException; -import java.util.Properties; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.wst.json.core.JSONCorePlugin; -import org.eclipse.wst.json.core.cleanup.CleanupProcessorJSON; -import org.eclipse.wst.json.core.format.FormatProcessorJSON; -import org.eclipse.wst.json.core.internal.preferences.JSONCorePreferenceInitializer; -import org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.wtp.sse.CleanupStep; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** - * Formatter step which calls out to the Eclipse JSON cleanup processor and formatter. - * Note that the cleanup is escaped, since it has known bugs and is currently not used by Eclipse. - */ -public class EclipseJsonFormatterStepImpl extends CleanupStep { - - public EclipseJsonFormatterStepImpl(Properties properties) throws Exception { - super(new CleanupProcessor(), new FrameworkConfig(properties)); - PluginPreferences.assertNoChanges(JSONCorePlugin.getDefault(), properties); - } - - /** - * The JSON CleanUp is partly implemented. - *

- * For example the abstract formatter supports the - * CASE_PROPERTY_NAME configuration item. - * However, this seems to be all dead code and there seems - * to be no way in the latest Eclipse GUI to configure - * or trigger the clean-up process. - *

- *

- * Here we just use the CleanupProcessorJSON to reuse the common - * interface to trigger the formatting. - *

- * See {@code org.eclipse.wst.json.core.internal.format.AbstractJSONSourceFormatter} for details. - */ - private static class CleanupProcessor extends CleanupProcessorJSON implements CleanupStep.ProcessorAccessor { - private final FormatProcessorJSON formatter; - - CleanupProcessor() { - formatter = new FormatProcessorJSON(); - } - - @Override - public String getTypeId() { - return getContentType(); - } - - @Override - public AbstractStructuredCleanupProcessor get() { - return this; - } - - @Override - public void refreshPreferences() { - refreshCleanupPreferences(); - } - - @Override - public String cleanupContent(String input) throws IOException, CoreException { - /* - * The CleanupProcessorJSON.cleanupContent is erroneous and disabled in IDE. - * Hence the clean-up itself is replaced by a format processor. - * The SpotlessJsonCleanup still derives from the CleanupStep base class - * to use the common Spotless WTP configuration. - * - * See Spotless issue #344 for details. - */ - return formatter.formatContent(input); - } - } - - private static class FrameworkConfig extends CleanupStep.FrameworkConfig { - private final Properties properties; - - FrameworkConfig(Properties properties) { - this.properties = properties; - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - super.activatePlugins(config); - config.add(new JSONCorePlugin()); - } - - @Override - public void customize() { - PluginPreferences.configure(JSONCorePlugin.getDefault(), new JSONCorePreferenceInitializer(), properties); - } - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImpl.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImpl.java deleted file mode 100644 index 57fce0d503..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImpl.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.*; - -import java.util.Properties; - -import org.eclipse.jface.text.IDocumentPartitioner; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin; -import org.eclipse.wst.dtd.core.internal.DTDCorePlugin; -import org.eclipse.wst.sse.core.internal.provisional.INodeAdapterFactory; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.sse.core.internal.text.BasicStructuredDocument; -import org.eclipse.wst.xml.core.internal.XMLCorePlugin; -import org.eclipse.wst.xml.core.internal.catalog.Catalog; -import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.CMDocumentManager; -import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery; -import org.eclipse.wst.xml.core.internal.document.DOMModelImpl; -import org.eclipse.wst.xml.core.internal.formatter.DefaultXMLPartitionFormatter; -import org.eclipse.wst.xml.core.internal.formatter.XMLFormattingPreferences; -import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryAdapterFactoryForXML; -import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil; -import org.eclipse.wst.xml.core.internal.parser.XMLSourceParser; -import org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceInitializer; -import org.eclipse.wst.xml.core.internal.text.rules.StructuredTextPartitionerForXML; -import org.eclipse.wst.xsd.core.internal.XSDCorePlugin; -import org.eclipse.xsd.util.XSDSchemaBuildingTools; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; -import com.diffplug.spotless.extra.eclipse.wtp.sse.PreventExternalURIResolverExtension; - -/** Formatter step which calls out to the Eclipse XML formatter. */ -public class EclipseXmlFormatterStepImpl { - private final DefaultXMLPartitionFormatter formatter; - private final XMLFormattingPreferences preferences; - private final INodeAdapterFactory xmlAdapterFactory; - - public EclipseXmlFormatterStepImpl(Properties properties) throws Exception { - SpotlessEclipseFramework.setup(new FrameworkConfig(properties)); - PluginPreferences.assertNoChanges(XMLCorePlugin.getDefault(), properties); - preferences = new XMLFormattingPreferences(); - formatter = new DefaultXMLPartitionFormatter(); - //The adapter factory maintains the common CMDocumentCache - xmlAdapterFactory = new ModelQueryAdapterFactoryForXML(); - } - - static class FrameworkConfig implements SpotlessEclipseConfig { - private final Properties properties; - - FrameworkConfig(Properties properties) { - /* - * The cache is only used for system catalogs, but not for user catalogs. - * It requires the SSECorePLugin, which has either a big performance overhead, - * or needs a dirty mocking (we don't really require its functions but it needs to be there). - * So we disable the cache for now. - * This might cause a performance drop in case for example XSDs are formatted. - * But these standard/system restriction files contain anyway no formatting rules. - * So in case of performance inconveniences, we could add a Spotless property to disable the - * XSD/DTD parsing entirely (for example by adding an own URI resolver). - */ - properties.setProperty(CMDOCUMENT_GLOBAL_CACHE_ENABLED, Boolean.toString(false)); - this.properties = properties; - } - - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(EclipseXmlFormatterStepImpl.class.getPackage().getName()); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - activateXmlPlugins(config, PluginPreferences.isExternalUriAllowed(properties)); - } - - static void activateXmlPlugins(SpotlessEclipsePluginConfig config, boolean allowExternalURI) { - //The WST XML formatter - config.add(new XMLCorePlugin()); - //XSDs/DTDs must be resolved by URI - config.add(new URIResolverPlugin()); - //Support formatting based on DTD restrictions - config.add(new DTDCorePlugin()); - //Support formatting based on XSD restrictions - config.add(new XSDCorePlugin()); - if (!allowExternalURI) { - config.add(new PreventExternalURIResolverExtension()); - } - } - - @Override - public void customize() { - //Register required EMF factories - XSDSchemaBuildingTools.getXSDFactory(); - PluginPreferences.configure(XMLCorePlugin.getDefault(), new XMLCorePreferenceInitializer(), properties); - PluginPreferences.configureCatalog(properties, (Catalog) XMLCorePlugin.getDefault().getDefaultXMLCatalog()); - } - - }; - - /** Formatting XML string resolving URIs according its base location */ - public String format(String raw, String baseLocation) throws Exception { - IStructuredDocument document = new BasicStructuredDocument(new XMLSourceParser()); - document.setPreferredLineDelimiter(LINE_DELIMITER); - IDocumentPartitioner partitioner = new StructuredTextPartitionerForXML(); - document.setDocumentPartitioner(new StructuredTextPartitionerForXML()); - partitioner.connect(document); - document.set(raw); - DOMModelImpl xmlDOM = new DOMModelImpl(); - xmlDOM.setBaseLocation(baseLocation); - xmlDOM.getFactoryRegistry().addFactory(xmlAdapterFactory); - xmlDOM.setStructuredDocument(document); - ModelQuery modelQuery = ModelQueryUtil.getModelQuery(xmlDOM); - modelQuery.getCMDocumentManager().setPropertyEnabled(CMDocumentManager.PROPERTY_USE_CACHED_RESOLVED_URI, true); - TextEdit formatterChanges = formatter.format(xmlDOM, 0, document.getLength(), preferences); - formatterChanges.apply(document); - return document.get(); - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/JsRegionProcessor.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/JsRegionProcessor.java deleted file mode 100644 index d856335c1b..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/JsRegionProcessor.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.html; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; - -import java.util.function.BiFunction; - -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.ITypedRegion; -import org.eclipse.text.edits.MalformedTreeException; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.jsdt.core.formatter.CodeFormatter; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; - -import com.diffplug.spotless.extra.eclipse.wtp.html.StructuredDocumentProcessor.RegionProcessor; - -/** - * Provides additional formating to the plain JS {@link CodeFormatter}: - *
    - *
  • Eclipse HTML places the embedded JS in separated lines by adding a line break after/before <script/> tag.
  • - *
  • Eclipse HTML treats the text before the closing </script> tag as part of the script region.
  • - *
- *

- * Note that the closing tag is indented by Eclipse using the embedded formatters indentation, - * whereas the opening tag indentation is configured by the HTML preferences. - * This is more a bug than a feature, but Spotless formatter output shall be identical - * to the one of Eclipse. - *

- */ -public class JsRegionProcessor extends RegionProcessor { - public JsRegionProcessor(IStructuredDocument document, ITypedRegion scriptRegion, String htmlIndent) { - super(document, scriptRegion, htmlIndent); - } - - @Override - protected void applyFirst(CodeFormatter formatter) throws MalformedTreeException, BadLocationException { - MultiTextEdit modifications = new MultiTextEdit(); - String jsSource = document.get(region.getOffset(), region.getLength()); - TextEdit jsEdit = formatter.format(CodeFormatter.K_JAVASCRIPT_UNIT, jsSource, 0, jsSource.length(), indentationLevel + 1, LINE_DELIMITER); - if (null != jsEdit) { - jsEdit.moveTree(region.getOffset()); - modifications.addChild(jsEdit); - } - modifications.apply(document); - } - - @Override - protected void applySecond(CodeFormatter formatter) throws MalformedTreeException, BadLocationException { - MultiTextEdit modifications = new MultiTextEdit(); - int regionEnd = region.getOffset() + region.getLength(); - regionEnd += fixDelimiter(modifications, region.getOffset(), false); - regionEnd += fixDelimiter(modifications, region.getOffset() + region.getLength() - 1, true); - modifications.apply(document); - modifications.removeChildren(); - fixTagIndent(modifications, regionEnd, formatter.createIndentationString(indentationLevel)); - modifications.apply(document); - } - - /** Factory for {@link StructuredDocumentProcessor}*/ - public static BiFunction createFactory(String htmlIndent) { - return new BiFunction() { - - @Override - public JsRegionProcessor apply(IStructuredDocument document, ITypedRegion region) { - return new JsRegionProcessor(document, region, htmlIndent); - } - - }; - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/StructuredDocumentProcessor.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/StructuredDocumentProcessor.java deleted file mode 100644 index 6adc258c78..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/StructuredDocumentProcessor.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.html; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiFunction; -import java.util.stream.Collectors; - -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.ITypedRegion; -import org.eclipse.text.edits.InsertEdit; -import org.eclipse.text.edits.MalformedTreeException; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.ReplaceEdit; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; - -/** - * The HTML formatter uses for different regions of the structured document, - * other formatters, explicitly for CSS and JS. - * Adaptations of the current formatter modifications are tedious, - * since the adaptations e.g. overlap with the required additional modifications. - * Hence the modifications is split in steps, whereas the regions of the - * structured documents and their offsets are regenerate between the first - * ad second step. - */ -public class StructuredDocumentProcessor { - private final String type; - private final BiFunction> factory; - private final IStructuredDocument document; - private final int numberOfRegions; - - /** - * Constructs a document processor - * @param document Document to be processed - * @param type Document type ID recognized by {@code IContentTypeManager} service - * @param factory Factory for structured document processor - */ - public StructuredDocumentProcessor(IStructuredDocument document, String type, - BiFunction> factory) { - this.type = type; - this.factory = factory; - this.document = document; - numberOfRegions = getRegions().size(); - } - - /** Applies processor on document, using a given formatter */ - public void apply(T formatter) { - for (int currentRegionId = 0; currentRegionId < numberOfRegions; currentRegionId++) { - applyOnRegion(currentRegionId, formatter); - } - } - - private List getRegions() { - try { - return Arrays.asList(document.computePartitioning(0, document.getLength())).stream().filter(reg -> type == reg.getType()).collect(Collectors.toList()); - } catch (BadLocationException e) { - /* - * This prevents the processing in case the entire document - * cannot be processed (e.g. the document is incomplete). - */ - return new ArrayList(0); - } - } - - private void applyOnRegion(int number, T formatter) { - RegionProcessor adapter = getRegionProcessor(number); - try { - adapter.applyFirst(formatter); - adapter = getRegionProcessor(number); - adapter.applySecond(formatter); - } catch (MalformedTreeException | BadLocationException e) { - throw new IllegalArgumentException( - String.format("%s formatting failed between lines %d and %d. Most likely the syntax is not recognized.", - type, adapter.getFirstLine(), adapter.getLastLine()), - e); - } - } - - private RegionProcessor getRegionProcessor(int number) { - List regions = getRegions(); - if (numberOfRegions != regions.size()) { - //Don't catch this. This is a severe internal bug! - throw new IllegalArgumentException( - String.format( - "During first '%s' formatting step, the number of detected regions changed from '%d' to '%d'", - type, numberOfRegions, regions.size())); - } - ITypedRegion region = regions.get(number); - return factory.apply(document, region); - } - - /** Base class for region adaptations. */ - public static abstract class RegionProcessor { - protected final IStructuredDocument document; - protected final ITypedRegion region; - protected final int indentationLevel; - protected final int firstLine; - protected final int lastLine; - - protected RegionProcessor(IStructuredDocument document, ITypedRegion region, String htmlIndent) { - this.document = document; - this.region = region; - indentationLevel = computeIndent(document, region, htmlIndent); - firstLine = document.getLineOfOffset(region.getOffset()); - lastLine = document.getLineOfOffset(region.getOffset() + region.getLength()); - } - - public int getFirstLine() { - return firstLine; - } - - public int getLastLine() { - return lastLine; - } - - private static int computeIndent(IStructuredDocument document, ITypedRegion region, String htmlIndent) { - int indent = 0; - try { - int lineNumber = document.getLineOfOffset(region.getOffset()); - document.getNumberOfLines(); - int lineOffset = document.getLineOffset(lineNumber); - String lineStart = document.get(lineOffset, region.getOffset() - lineOffset); - while (lineStart.length() > htmlIndent.length()) { - if (lineStart.startsWith(htmlIndent)) { - indent++; - } else { - break; - } - lineStart = lineStart.substring(htmlIndent.length()); - } - } catch (BadLocationException e) { - /* - * Skip addition indentation. This normally indicates a malformed HTML - * outside of this region, which cannot be handled here. - */ - indent = 0; - } - return indent; - } - - /** Add delimiter at or after given offset, if there is none at the offset position. Returns the number of characters inserted. */ - protected int fixDelimiter(MultiTextEdit modifications, int offset, boolean addAfter) throws BadLocationException { - int delimiterLength = LINE_DELIMITER.length(); - String delimiter = document.get(offset, delimiterLength); - if (!LINE_DELIMITER.equals(delimiter)) { - if (addAfter) { - offset += 1; - } - modifications.addChild(new InsertEdit(offset, LINE_DELIMITER)); - return LINE_DELIMITER.length(); - } - return 0; - } - - /** Fix the tag indentation at a given position with a predefined indentation. */ - protected void fixTagIndent(MultiTextEdit modifications, int offset, String indentString) throws BadLocationException { - int lineNumber = document.getLineOfOffset(offset); - if (lineNumber >= document.getNumberOfLines()) { - //Nothing to change for last line. If syntax is correct, there is no indentation. If syntax is not correct, there is nothing to do. - return; - } - int lineStart = document.getLineOffset(lineNumber); - int lineEnd = document.getLineOffset(lineNumber + 1); - String lineContent = document.get(lineStart, lineEnd - lineStart); - StringBuilder currentIndent = new StringBuilder(); - lineContent.chars().filter(c -> { - if (c == ' ' || c == '\t') { - currentIndent.append(c); - return false; - } - return true; - }).findFirst(); - if (!indentString.equals(currentIndent.toString())) { - TextEdit replaceIndent = new ReplaceEdit(lineStart, currentIndent.length(), indentString); - replaceIndent.apply(document); - } - } - - /** First application of modifications */ - abstract protected void applyFirst(T formatter) throws MalformedTreeException, BadLocationException; - - /** Second application of modifications (based on new regions) */ - abstract protected void applySecond(T formatter) throws MalformedTreeException, BadLocationException; - - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/CleanupStep.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/CleanupStep.java deleted file mode 100644 index 01be583bd8..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/CleanupStep.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.sse; - -import org.eclipse.core.internal.preferences.PreferencesService; -import org.eclipse.core.runtime.content.IContentTypeManager; -import org.eclipse.core.runtime.preferences.IPreferencesService; -import org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor; - -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; - -/** - * Common base class for step implementations based on an SSE cleanup processor. - *

- * Some WTP formatters do not apply all formatting within a formatter process, - * but provide a cleanup and a formatter process, whereas the cleanup process - * calls the formatter process. - *

- *

- * Not all cleanup processes provided by WTP are suitable for Spotless formatting. - * For example the XML cleanup processor focus on syntax and line delimiter - * corrections. The syntax correction is based on the corresponding XSD/DTD, - * but it must be assumed by the Spotless formatter that the provided files - * are valid. It cannot be the purpose of a formatter to correct syntax. - * A Java formatter should also not attempt to correct compilation errors. - * A line delimiter correction would furthermore violate the Spotless rule - * that the strings processed by a Spotless formatter chain must use - * UNIX style delimiters. - *

- * @see org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor - */ -public class CleanupStep { - - /** Make some of the protected SEE AbstractStructuredCleanupProcessor methods public. */ - public interface ProcessorAccessor { - - /** Configure from Eclipse framework preferences */ - void refreshPreferences(); - - /** Get underlying processor */ - AbstractStructuredCleanupProcessor get(); - - /** Get processor content type */ - String getTypeId(); - } - - /** Framework configuration public to all formatters using the Cleanup step */ - public static abstract class FrameworkConfig implements SpotlessEclipseConfig { - - private String processorContentTypeID; - - protected FrameworkConfig() { - processorContentTypeID = "none"; - } - - void setProcessorTypeID(String contentTypeID) { - processorContentTypeID = contentTypeID; - } - - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.disableDebugging(); - config.hideEnvironment(); - config.useTemporaryLocations(); - config.changeSystemLineSeparator(); - config.useSlf4J(this.getClass().getPackage().getName() + "-" + processorContentTypeID); - - //Assure that all file content processed by this formatter step are associated to this cleanup-step - config.add(IContentTypeManager.class, new ContentTypeManager(processorContentTypeID)); - - //The preference lookup via the ContentTypeManager, requires a preference service - config.add(IPreferencesService.class, PreferencesService.getDefault()); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - /* - * The core preferences require do lookup the resources "config/override.properties" - * from the plugin ID. - * The values are never used, nor do we require the complete SSE core plugin to be started. - * Hence we just provide the internal plugin. - */ - config.add(new org.eclipse.wst.sse.core.internal.encoding.util.CodedResourcePlugin()); - } - } - - protected final ProcessorAccessor processorAccessor; - - protected CleanupStep(ProcessorAccessor processorAccessor, FrameworkConfig config) throws Exception { - config.setProcessorTypeID(processorAccessor.getTypeId()); - SpotlessEclipseFramework.setup(config); - this.processorAccessor = processorAccessor; - this.processorAccessor.refreshPreferences(); - /* - * Don't refresh the preferences every time a clone of the processor is created. - * All processors shall use the preferences of its parent. - */ - processorAccessor.get().refreshCleanupPreferences = false; - } - - /** - * Calls the cleanup and formatting task of the processor and returns the formatted string. - * @param raw Dirty string - * @return Formatted string - * @throws Exception All exceptions are considered fatal to the build process (Gradle, Maven, ...) and should not be caught. - */ - public String format(String raw) throws Exception { - return processorAccessor.get().cleanupContent(raw); - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/ContentTypeManager.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/ContentTypeManager.java deleted file mode 100644 index 499a147ae9..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/ContentTypeManager.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.sse; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.QualifiedName; -import org.eclipse.core.runtime.content.IContentDescription; -import org.eclipse.core.runtime.content.IContentType; -import org.eclipse.core.runtime.content.IContentTypeSettings; -import org.eclipse.core.runtime.preferences.IScopeContext; -import org.eclipse.wst.css.core.internal.provisional.contenttype.ContentTypeIdForCSS; -import org.eclipse.wst.html.core.internal.provisional.contenttype.ContentTypeIdForHTML; -import org.eclipse.wst.json.core.contenttype.ContentTypeIdForJSON; -import org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML; - -import com.diffplug.spotless.extra.eclipse.base.service.NoContentTypeSpecificHandling; - -/** - - * WTP ModelHandlerRegistry uses the content type mamanger clean-up formatters - * to provide association of content to content type related functionality. - *

- * Preference lookup per content type is accomplished via the - * Eclipse PreferencesService, which must be provided in combination with - * this service. - *

- * The input byte steam encoding detection is accomplished by the - * content type manager. Normally the encoding is bount do a document/file. - * Spotless applies the formatting on strings already decoded. - * The WTP AbstractStructuredCleanupProcessor provides for non-documents - * a clean-up function converting the decoded string into an UTF-8 encoded byte stream. - * WTP AbstractDocumentLoader uses content type mamanger to determine the encoding - * of the input stream. - * Only the steps are affected that are using the - * AbstractStructuredCleanupProcessor. All other steps creating an empty document - * (e.g. via WTP AbstractDocumentLoader) and setting the textual content of the new document. - * - * @see org.eclipse.core.internal.preferences.PreferencesService - * @see org.eclipse.wst.sse.core.internal.cleanup.AbstractStructuredCleanupProcessor - * @see org.eclipse.wst.sse.core.internal.document.AbstractDocumentLoader - * @see org.eclipse.wst.sse.core.internal.modelhandler.ModelHandlerRegistry - */ -class ContentTypeManager extends NoContentTypeSpecificHandling { - private final Map id2Object; - private final IContentType processorStepType; - private final IContentDescription processorStepDescription; - - /** - * Content type manager as required for cleanup steps. - * @param formatterContentTypeID The content type of the formatter step - */ - ContentTypeManager(String formatterContentTypeID) { - id2Object = new HashMap(); - Arrays.asList( - ContentTypeIdForCSS.ContentTypeID_CSS, - ContentTypeIdForXML.ContentTypeID_XML, - ContentTypeIdForHTML.ContentTypeID_HTML, - ContentTypeIdForJSON.ContentTypeID_JSON) - .stream().forEach(id -> id2Object.put(id, new ContentTypeId(id))); - processorStepType = id2Object.get(formatterContentTypeID); - if (null == processorStepType) { - throw new IllegalArgumentException("The manager does not support content type " + formatterContentTypeID); - } - processorStepDescription = new StringDescription(processorStepType); - } - - @Override - public IContentType getContentType(String contentTypeIdentifier) { - /* - * It is OK to return null here since the manager is only used as an additional - * helper to alter default behavior. - */ - return id2Object.get(contentTypeIdentifier); - } - - @Override - public IContentType findContentTypeFor(InputStream contents, String fileName) throws IOException { - //We only format things here with the given processor, so this answer is always correct. - return processorStepType; - } - - @Override - public IContentDescription getDescriptionFor(InputStream contents, String fileName, QualifiedName[] options) throws IOException { - return processorStepDescription; - } - - private static class StringDescription implements IContentDescription { - - private final IContentType type; - - public StringDescription(IContentType type) { - this.type = type; - } - - @Override - public boolean isRequested(QualifiedName key) { - return false; //Don't use set Property - } - - @Override - public String getCharset() { - //Called by AbstractDocumentLoader.readInputStream - return "UTF-8"; //UTF-8 encoded by AbstractStructuredCleanupProcessor.cleanupContent - } - - @Override - public IContentType getContentType() { - return type; - } - - @Override - public Object getProperty(QualifiedName key) { - return null; //Assume that the property map is empty - } - - @Override - public void setProperty(QualifiedName key, Object value) { - throw new IllegalArgumentException("Content description key cannot be set: " + key); - } - } - - /** - * The WTP uses the manager mainly for ID mapping, so most of the methods are not used. - * Actually it has a hand stitched way for transforming the content type ID - * {@code org.eclipse.wst...source} to the plugin ID {@code org.eclipse.wst...core}. - * @see org.eclipse.wst.sse.core.internal.encoding.ContentBasedPreferenceGateway - */ - private static class ContentTypeId implements IContentType { - - private final String id; - - ContentTypeId(String id) { - this.id = id; - } - - @Override - public void addFileSpec(String fileSpec, int type) throws CoreException {} - - @Override - public void removeFileSpec(String fileSpec, int type) throws CoreException {} - - @Override - public void setDefaultCharset(String userCharset) throws CoreException {} - - @Override - public boolean isUserDefined() { - return false; - } - - @Override - public IContentType getBaseType() { - return null; - } - - @Override - public IContentDescription getDefaultDescription() { - return null; - } - - @Override - public IContentDescription getDescriptionFor(InputStream contents, QualifiedName[] options) throws IOException { - return null; - } - - @Override - public IContentDescription getDescriptionFor(Reader contents, QualifiedName[] options) throws IOException { - return null; - } - - @Override - public String getDefaultCharset() { - return null; - } - - @Override - public String[] getFileSpecs(int type) { - return null; - } - - @Override - public String getId() { - return id; - } - - @Override - public String getName() { - return id; - } - - @Override - public boolean isAssociatedWith(String fileName) { - return false; - } - - @Override - public boolean isAssociatedWith(String fileName, IScopeContext context) { - return false; - } - - @Override - public boolean isKindOf(IContentType another) { - if (null == another) { - return false; - } - return this.id.equals(another.getId()); - } - - @Override - public IContentTypeSettings getSettings(IScopeContext context) throws CoreException { - return null; - } - - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PluginPreferences.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PluginPreferences.java deleted file mode 100644 index 8e101ec87c..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PluginPreferences.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.sse; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; - -import org.eclipse.core.runtime.Plugin; -import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; -import org.eclipse.core.runtime.preferences.DefaultScope; -import org.eclipse.core.runtime.preferences.IEclipsePreferences; -import org.eclipse.wst.xml.core.internal.catalog.Catalog; -import org.eclipse.wst.xml.core.internal.catalog.CatalogReader; -import org.eclipse.wst.xml.core.internal.catalog.provisional.ICatalog; - -/** - * The plugin preference configuration of most WTP formatters is accomplished via the the - * globabl Eclipse preference lookup. - * Spotless allows different formatter configurations per sub-projects. - * Fortunately most formatters only perform a lookup on instantiation and not afterwards. - * This is sometimes not true for all preferences of the formatter. - * - * - */ -public class PluginPreferences { - /** - * Optional XML catalog for XSD/DTD lookup. - * Catalog versions 1.0 and 1.1 are supported. - *

- * Value is of type {@code Path}. - *

- */ - public static final String USER_CATALOG = "userCatalog"; - - /** - * Indicates if external URIs (location hints) should be resolved - * and the referenced DTD/XSD shall be applied. Per default - * external URIs are ignored. - *

- * Value is of type Boolean. - *

- */ - public static final String RESOLVE_EXTERNAL_URI = "resolveExternalURI"; - - /** Storage of latest global configuration */ - private static final Map CONFIG = new HashMap<>(); - - /** \return True if user explicitly set the */ - public static boolean isExternalUriAllowed(Properties properties) { - return Boolean.parseBoolean(properties.getProperty(PluginPreferences.RESOLVE_EXTERNAL_URI, "false")); - } - - /** Configures persistent Eclipse properties */ - public static void configure(Plugin plugin, AbstractPreferenceInitializer defaultInitializer, Properties properties) { - defaultInitializer.initializeDefaultPreferences(); - IEclipsePreferences globalPreferences = DefaultScope.INSTANCE.getNode(getPluginId(plugin)); - properties.forEach((key, value) -> { - globalPreferences.put((String) key, (String) value); - }); - store(plugin, properties); - } - - /** Stores Eclipse properties for later comparison */ - public static void store(Plugin plugin, Properties properties) { - CONFIG.put(getPluginId(plugin), properties); - } - - /** Keep in mind that the ID is e.g. equal for all plugins. So assure that all user properties ge stored. */ - private static String getPluginId(Plugin plugin) { - return plugin.getBundle().getSymbolicName(); - } - - public static void configureCatalog(final Properties properties, final ICatalog defaultCatalogInterface) { - Objects.requireNonNull(properties, "Property values are missing."); - Objects.requireNonNull(defaultCatalogInterface, "Default catalog missing."); - if (!(defaultCatalogInterface instanceof Catalog)) { - throw new IllegalArgumentException("Internal error: Catalog implementation '" + defaultCatalogInterface.getClass().getCanonicalName() + "' unsupported."); - } - Catalog defaultCatalog = (Catalog) defaultCatalogInterface; - String catalogProperty = properties.getProperty(USER_CATALOG, ""); - if (!catalogProperty.isEmpty()) { - final File catalogFile = new File(catalogProperty); - try { - InputStream inputStream = new FileInputStream(catalogFile); - String orgBase = defaultCatalog.getBase(); - defaultCatalog.setBase(catalogFile.toURI().toString()); - CatalogReader.read((Catalog) defaultCatalog, inputStream); - defaultCatalog.setBase(orgBase); - } catch (IOException e) { - throw new IllegalArgumentException( - String.format("Value of '%s' refers to '%s', which cannot be read.", USER_CATALOG, catalogFile)); - } - } else { - defaultCatalog.clear(); - } - } - - /** Throws exception in case configuration has changed */ - public static void assertNoChanges(Plugin plugin, Properties properties) { - Objects.requireNonNull(properties, "Property values are missing."); - final String preferenceId = plugin.getBundle().getSymbolicName(); - Properties originalValues = CONFIG.get(preferenceId); - if (null == originalValues) { - throw new IllegalArgumentException("No configuration found for " + preferenceId); - } - if (!originalValues.equals(properties)) { - throw new IllegalArgumentException("The Eclipse formatter does not support multiple configurations."); - } - } - -} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PreventExternalURIResolverExtension.java b/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PreventExternalURIResolverExtension.java deleted file mode 100644 index d0b95e5632..0000000000 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/PreventExternalURIResolverExtension.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp.sse; - -import org.eclipse.core.resources.IFile; -import org.eclipse.emf.common.util.URI; -import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverExtension; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * The URI resolver extension - */ -public class PreventExternalURIResolverExtension implements URIResolverExtension, BundleActivator { - - private static final String REFUSE_EXTERNAL_URI = "file://refused.external.uri"; - - /** - * @param file the in-workspace base resource, if one exists - * @param baseLocation - the location of the resource that contains the uri - * @param publicId - an optional public identifier (i.e. namespace name), or null if none - * @param systemId - an absolute or relative URI, or null if none - * - * @return an absolute URI or null if this extension can not resolve this reference - */ - @Override - public String resolve(IFile file, String baseLocation, String publicId, String systemId) { - if (null != systemId) { - try { - URI proposalByPreviousResolver = org.eclipse.emf.common.util.URI.createURI(systemId); - String host = proposalByPreviousResolver.host(); - /* - * The host is empty (not null) - */ - if (!(null == host || host.isEmpty())) { - return REFUSE_EXTERNAL_URI; - } - } catch (IllegalArgumentException ignore) { - //If it is no a valid URI, there is nothing to do here. - } - } - return null; //Don't alter the proposal of previous resolver extensions by proposing something else - } - - @Override - public void start(BundleContext context) throws Exception { - //Nothing to do. The bundle-activator interface only allows to load this extension as a stand-alone plugin. - } - - @Override - public void stop(BundleContext context) throws Exception { - //Nothing to do. The bundle-activator interface only allows to load this extension as a stand-alone plugin. - } - -} diff --git a/_ext/eclipse-wtp/src/main/resources/META-INF/MANIFEST.MF b/_ext/eclipse-wtp/src/main/resources/META-INF/MANIFEST.MF deleted file mode 100644 index 9f785fea6c..0000000000 --- a/_ext/eclipse-wtp/src/main/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-SymbolicName: com.diffplug.gradle.spotless.eclipse.wtp; singleton:=true diff --git a/_ext/eclipse-wtp/src/main/resources/plugin.xml b/_ext/eclipse-wtp/src/main/resources/plugin.xml deleted file mode 100644 index 7ffd251c94..0000000000 --- a/_ext/eclipse-wtp/src/main/resources/plugin.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImplTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImplTest.java deleted file mode 100644 index 8dda3149c0..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseCssFormatterStepImplTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.wst.css.core.internal.preferences.CSSCorePreferenceNames.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class EclipseCssFormatterStepImplTest { - - private final static String ILLEGAL_CHAR = Character.toString((char) 254); - private final static String UNFORMATTED = " body {a: v; b: v;}\n".replaceAll("\n", LINE_DELIMITER); - private final static String FORMATTED = "BODY {\n a: v;\n b: v;\n}".replaceAll("\n", LINE_DELIMITER); - private final static String PRE_CODE_UNFORMATTED = "/**
\"Hello\"
*/\n".replaceAll("\n", LINE_DELIMITER); - - private EclipseCssFormatterStepImpl formatter; - - @BeforeEach - void initialize() throws Exception { - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.css.core.prefs. - */ - Properties properties = new Properties(); - properties.put(INDENTATION_SIZE, "3"); - properties.put(INDENTATION_CHAR, SPACE); //Default is TAB - properties.put(CLEANUP_CASE_SELECTOR, Integer.toString(UPPER)); //Done by cleanup - formatter = new EclipseCssFormatterStepImpl(properties); - } - - @Test - void format() throws Exception { - String output = formatter.format(UNFORMATTED); - assertEquals(FORMATTED, - output, "Unexpected formatting with default preferences."); - } - - @Test - void illegalCharacter() throws Exception { - String output = formatter.format(ILLEGAL_CHAR + UNFORMATTED); - assertThat(output).as("Illeagl characters are not handled on best effort basis.").contains("BODY {"); - } - - @Test - void illegalSyntax() throws Exception { - String output = formatter.format("{" + UNFORMATTED); - assertEquals("{" + LINE_DELIMITER + FORMATTED, - output, "Illeagl syntax is not handled on best effort basis."); - } - - @Test - void formatComment() throws Exception { - String output = formatter.format(PRE_CODE_UNFORMATTED + UNFORMATTED); - assertEquals(PRE_CODE_UNFORMATTED + FORMATTED, - output, "Unexpected formatting of cpomments."); - } - - @Test - void configurationChange() throws Exception { - assertThrows(IllegalArgumentException.class, () -> new EclipseCssFormatterStepImpl(new Properties())); - } - -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImplTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImplTest.java deleted file mode 100644 index b75421692a..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseHtmlFormatterStepImplTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static org.eclipse.wst.html.core.internal.preferences.HTMLCorePreferenceNames.*; -import static org.eclipse.wst.jsdt.core.formatter.DefaultCodeFormatterConstants.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.eclipse.wst.html.core.internal.preferences.HTMLCorePreferenceNames; -import org.eclipse.wst.jsdt.core.JavaScriptCore; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class EclipseHtmlFormatterStepImplTest { - - private TestData testData = null; - private EclipseHtmlFormatterStepImpl formatter; - - @BeforeEach - void initialize() throws Exception { - testData = TestData.getTestDataOnFileSystem("html"); - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.xml.core.prefs. - * So a simple test of one configuration item change is considered sufficient. - */ - Properties properties = new Properties(); - properties.put(CLEANUP_TAG_NAME_CASE, Integer.toString(HTMLCorePreferenceNames.UPPER)); //HTML config - properties.put(FORMATTER_INSERT_SPACE_BEFORE_SEMICOLON, JavaScriptCore.INSERT); //JS config - properties.put(QUOTE_ATTR_VALUES, "TRUE"); //CSS config - formatter = new EclipseHtmlFormatterStepImpl(properties); - } - - @Test - void formatHtml4() throws Exception { - String[] input = testData.input("html4.html"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("html4.html"), - output, "Unexpected HTML4 formatting."); - } - - @Test - void formatHtml5() throws Exception { - String[] input = testData.input("html5.html"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("html5.html"), - output, "Unexpected HTML5 formatting."); - } - - @Test - void invalidSyntax() throws Exception { - String[] input = testData.input("invalid_syntax.html"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("invalid_syntax.html"), - output, "Unexpected HTML formatting in case syntax is not valid."); - } - - @Test - void formatJavaScript() throws Exception { - String[] input = testData.input("javascript.html"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("javascript.html"), - output, "Unexpected JS formatting."); - } - - @Test - void formatCSS() throws Exception { - String[] input = testData.input("css.html"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("css.html"), - output, "Unexpected CSS formatting."); - } - - @Test - void checkCleanupForNonUtf8() throws Exception { - String osEncoding = System.getProperty("file.encoding"); - System.setProperty("file.encoding", "ISO-8859-1"); //Simulate a non UTF-8 OS - String[] input = testData.input("utf-8.html"); - String output = formatter.format(input[0]); - System.setProperty("file.encoding", osEncoding); - assertEquals(testData.expected("utf-8.html"), output, "Unexpected formatting of UTF-8"); - } - - @Test - void checkBOMisStripped() throws Exception { - String[] input = testData.input("bom.html"); - String[] inputWithoutBom = testData.input("utf-8.html"); - //The UTF-8 BOM is interpreted as on UTF-16 character. - assertEquals(input[0].length() - 1, inputWithoutBom[0].length(), "BOM input invalid"); - String output = formatter.format(input[0]); - assertEquals(testData.expected("utf-8.html"), output, "BOM is not stripped"); - } - - @Test - void configurationChange() throws Exception { - assertThrows(IllegalArgumentException.class, () -> new EclipseHtmlFormatterStepImpl(new Properties())); - } -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImplTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImplTest.java deleted file mode 100644 index 0858c3a04d..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsFormatterStepImplTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Arrays; -import java.util.Properties; - -import org.eclipse.wst.jsdt.core.JavaScriptCore; -import org.eclipse.wst.jsdt.core.formatter.DefaultCodeFormatterConstants; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class EclipseJsFormatterStepImplTest { - private final static String ILLEGAL_CHAR = Character.toString((char) 254); - private final static String UNFORMATTED = "var TEST = TEST || {};\n" + - "TEST.say = function() {\n" + - " console.log(\"Hello world!\"); }\n".replaceAll("\n", LINE_DELIMITER); - private final static String FORMATTED = "var TEST = TEST || {};\n" + - "TEST.say = function () {\n" + - "\tconsole.log(\"Hello world!\");\n}\n".replaceAll("\n", LINE_DELIMITER); - // Single line comment remains untouched - private final static String SINGLE_LINE_COMMENT = "// One line \"hello world\"\n".replaceAll("\n", LINE_DELIMITER); - // JavaDoc comment get indentation. Within PPRE code, HTML entities are escaped. - private final static String PRE_CODE_UNFORMATTED = "/**
\"Hello\"
*/\n".replaceAll("\n", LINE_DELIMITER); - private final static String PRE_CODE_FORMATTED = "/**\n *
\n * "Hello"\n * 
\n */\n".replaceAll("\n", LINE_DELIMITER); - - private EclipseJsFormatterStepImpl formatter; - - @BeforeEach - void initialize() throws Exception { - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.jst.jsdt.core.prefs. - */ - Properties properties = new Properties(); - properties.setProperty(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR, JavaScriptCore.TAB); - formatter = new EclipseJsFormatterStepImpl(properties); - } - - @Test - void invalidSyntax() throws Exception { - IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> formatter.format(UNFORMATTED.replace("()", ""))); - assertThat(error.getMessage()).as("Exception has no hint about invalid syntax.").contains(Arrays.asList("Invalid", "syntax")); - } - - @Test - void illegalCharacter() throws Exception { - String output = formatter.format(UNFORMATTED.replace("function", "function" + ILLEGAL_CHAR)); - assertThat(output).as("Illegal ASCII charactes are not treated on best effort basis.").contains("function" + ILLEGAL_CHAR); - } - - @Test - void nominal() throws Exception { - String output = formatter.format(UNFORMATTED); - assertEquals(FORMATTED, output, "User configuration ignored by formatter."); - } - - @Test - void formatComments() throws Exception { - String output = formatter.format(SINGLE_LINE_COMMENT + PRE_CODE_UNFORMATTED + UNFORMATTED); - assertEquals(SINGLE_LINE_COMMENT + PRE_CODE_FORMATTED + FORMATTED, - output, "Invalid user configuration not treated on best effort basis."); - } - - @Test - void configurationChange() throws Exception { - assertThrows(IllegalArgumentException.class, () -> new EclipseJsFormatterStepImpl(new Properties())); - } - -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImplTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImplTest.java deleted file mode 100644 index 3bb3b7c988..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseJsonFormatterStepImplTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.eclipse.wst.json.core.preferences.JSONCorePreferenceNames.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class EclipseJsonFormatterStepImplTest { - private final static String ILLEGAL_CHAR = Character.toString((char) 254); - private final static String UNFORMATTED_OBJECT = "{\n \"x\": { \"a\" : \"v\",\"properties\" : \"v\" }}".replaceAll("\n", LINE_DELIMITER); - private final static String FORMATTED_OBJECT = "{\n \"x\": {\n \"a\": \"v\",\n \"properties\": \"v\"\n }\n}".replaceAll("\n", LINE_DELIMITER); - private final static String UNFORMATTED_ARRAY = "[\n { \"a\" : \"v\",\"properties\" : \"v\" }]".replaceAll("\n", LINE_DELIMITER); - private final static String FORMATTED_ARRAY = "[\n {\n \"a\": \"v\",\n \"properties\": \"v\"\n }\n]".replaceAll("\n", LINE_DELIMITER); - - private EclipseJsonFormatterStepImpl formatter; - - @BeforeEach - void initialize() throws Exception { - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.json.core.prefs. - * So a simple test of one configuration item change is considered sufficient. - */ - Properties properties = new Properties(); - properties.put(INDENTATION_SIZE, "3"); //Default is 1 - properties.put(INDENTATION_CHAR, SPACE); //Default is TAB - properties.put(CASE_PROPERTY_NAME, Integer.toString(UPPER)); //Dead code, ignored - formatter = new EclipseJsonFormatterStepImpl(properties); - } - - @Test - void formatObject() throws Exception { - String output = formatter.format(UNFORMATTED_OBJECT); - assertEquals(FORMATTED_OBJECT, - output, "Unexpected formatting with default preferences."); - } - - @Test - void formatArray() throws Exception { - String output = formatter.format(UNFORMATTED_ARRAY); - assertEquals(FORMATTED_ARRAY, - output, "Unexpected formatting with default preferences."); - } - - @Test - void illegalCharacter() throws Exception { - String output = formatter.format(ILLEGAL_CHAR + UNFORMATTED_OBJECT); - assertEquals(ILLEGAL_CHAR + FORMATTED_OBJECT, - output, "Illeagl characteds are not ignored."); - } - - @Test - void illegalSyntax() throws Exception { - String output = formatter.format("{" + UNFORMATTED_OBJECT); - assertEquals(FORMATTED_OBJECT, - output, "Illeagl syntax is not handled on best effort basis."); - } - - @Test - void configurationChange() throws Exception { - assertThrows(IllegalArgumentException.class, () -> new EclipseJsonFormatterStepImpl(new Properties())); - } -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplAllowExternalURIsTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplAllowExternalURIsTest.java deleted file mode 100644 index ac3b2c45f6..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplAllowExternalURIsTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.INDENTATION_CHAR; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.INDENTATION_SIZE; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.SPACE; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** Test configuration allowExternalURI=false */ -class EclipseXmlFormatterStepImplAllowExternalURIsTest { - private TestData testData = null; - private EclipseXmlFormatterStepImpl formatter; - - @BeforeEach - void initializeStatic() throws Exception { - testData = TestData.getTestDataOnFileSystem("xml"); - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.xml.core.prefs. - * So a simple test of one configuration item change is considered sufficient. - */ - Properties properties = new Properties(); - properties.put(PluginPreferences.RESOLVE_EXTERNAL_URI, "TRUE"); - properties.put(INDENTATION_SIZE, "2"); - properties.put(INDENTATION_CHAR, SPACE); //Default is TAB - formatter = new EclipseXmlFormatterStepImpl(properties); - } - - @Test - void dtdExternalPath() throws Throwable { - String[] input = testData.input("dtd_external.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("dtd_external.test"), - output, "External DTD not resolved. Restrictions are not applied by formatter."); - } - - @Test - void xsdExternalPath() throws Throwable { - String[] input = testData.input("xsd_external.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("xsd_external.test"), - output, "External XSD not resolved. Restrictions are not applied by formatter."); - } -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplCatalogLookupTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplCatalogLookupTest.java deleted file mode 100644 index a9ccd85720..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplCatalogLookupTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.INDENTATION_CHAR; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.INDENTATION_SIZE; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.SPACE; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.diffplug.spotless.extra.eclipse.wtp.sse.PluginPreferences; - -/** Test configuration allowExternalURI=false */ -class EclipseXmlFormatterStepImplCatalogLookupTest { - private TestData testData = null; - private EclipseXmlFormatterStepImpl formatter; - - @BeforeEach - void initializeStatic() throws Exception { - testData = TestData.getTestDataOnFileSystem("xml"); - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.xml.core.prefs. - * So a simple test of one configuration item change is considered sufficient. - */ - Properties properties = new Properties(); - properties.put(INDENTATION_SIZE, "2"); - properties.put(INDENTATION_CHAR, SPACE); //Default is TAB - properties.put(PluginPreferences.USER_CATALOG, testData.getRestrictionsPath("catalog.xml").toString()); - formatter = new EclipseXmlFormatterStepImpl(properties); - } - - @Test - void catalogLookup() throws Throwable { - String[] input = testData.input("xsd_not_found.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("xsd_not_found.test").replace(" remove spaces ", "remove spaces"), - output, "XSD not resolved by catalog. Restrictions are not applied by formatter."); - } -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplTest.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplTest.java deleted file mode 100644 index 88b15c42e2..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/EclipseXmlFormatterStepImplTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2016-2021 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.eclipse.wtp; - -import static com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework.LINE_DELIMITER; -import static org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Properties; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** Eclipse WST wrapper integration tests */ -class EclipseXmlFormatterStepImplTest { - - private final static String INCOMPLETE = ""; - private final static String ILLEGAL_CHAR = "\0"; - - private TestData testData = null; - private EclipseXmlFormatterStepImpl formatter; - - @BeforeEach - void initialize() throws Exception { - testData = TestData.getTestDataOnFileSystem("xml"); - /* - * The instantiation can be repeated for each step, but only with the same configuration - * All formatter configuration is stored in - * org.eclipse.core.runtime/.settings/org.eclipse.wst.xml.core.prefs. - * So a simple test of one configuration item change is considered sufficient. - */ - Properties properties = new Properties(); - properties.put(INDENTATION_SIZE, "2"); - properties.put(INDENTATION_CHAR, SPACE); //Default is TAB - formatter = new EclipseXmlFormatterStepImpl(properties); - } - - @Test - void simpleDefaultFormat() throws Throwable { - String[] input = testData.input("xml_space.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("xml_space.test"), - output, "Unexpected formatting with default preferences."); - } - - @Test - void invalidXmlFormat() throws Throwable { - String[] input = testData.input("xml_space.test"); - input[0] += INCOMPLETE; - String output = formatter.format(input[0], input[1]); - String expected = testData.expected("xml_space.test") + LINE_DELIMITER + INCOMPLETE; - assertEquals(expected, - output, "Incomplete XML not formatted on best effort basis."); - } - - @Test - void illegalXmlCharater() throws Throwable { - String[] input = testData.input("xml_space.test"); - input[0] = ILLEGAL_CHAR + input[0]; - String output = formatter.format(input[0], input[1]); - String expected = LINE_DELIMITER + LINE_DELIMITER + testData.expected("xml_space.test"); - assertEquals(expected, output, "Illegal character not replaced by line delimiter."); - } - - @Test - void dtdRelativePath() throws Throwable { - String[] input = testData.input("dtd_relative.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("dtd_relative.test"), - output, "Relative DTD not resolved. Restrictions are not applied by formatter."); - } - - @Test - void dtdExternalPath() throws Throwable { - String[] input = testData.input("dtd_external.test"); - String output = formatter.format(input[0], input[1]); - assertNotEquals(testData.expected("dtd_external.test"), - output, "External DTD resolved by default. Restrictions are applied by formatter."); - } - - @Test - void xsdRelativePath() throws Throwable { - String[] input = testData.input("xsd_relative.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("xsd_relative.test"), - output, "Relative XSD not resolved. Restrictions are not applied by formatter."); - } - - @Test - void xsdExternalPath() throws Throwable { - String[] input = testData.input("xsd_external.test"); - String output = formatter.format(input[0], input[1]); - assertNotEquals(testData.expected("xsd_external.test"), - output, "External XSD resolved per default. Restrictions are applied by formatter."); - } - - @Test - void xsdNotFound() throws Throwable { - String[] input = testData.input("xsd_not_found.test"); - String output = formatter.format(input[0], input[1]); - assertEquals(testData.expected("xsd_not_found.test"), - output, "Unresolved XSD/DTD not silently ignored."); - } - - @Test - void configurationChange() throws Exception { - assertThrows(IllegalArgumentException.class, () -> new EclipseXmlFormatterStepImpl(new Properties())); - } -} diff --git a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/TestData.java b/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/TestData.java deleted file mode 100644 index d767337970..0000000000 --- a/_ext/eclipse-wtp/src/test/java/com/diffplug/spotless/extra/eclipse/wtp/TestData.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2016 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.eclipse.wtp; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class TestData { - public static TestData getTestDataOnFileSystem(String kind) { - final String userDir = System.getProperty("user.dir", "."); - Path dataPath = Paths.get(userDir, "src", "test", "resources", kind); - if (Files.isDirectory(dataPath)) { - return new TestData(dataPath); - } - return null; - } - - private final Path inputPath; - private final Path expectedPath; - private final Path restrictionsPath; - - private TestData(Path dataPath) { - inputPath = dataPath.resolve("input").toAbsolutePath(); - expectedPath = dataPath.resolve("expected").toAbsolutePath(); - restrictionsPath = dataPath.resolve("restrictions").toAbsolutePath(); - for (Path testDataDir : new Path[]{inputPath, expectedPath, restrictionsPath}) { - if (!Files.isDirectory(testDataDir)) { - throw new IllegalArgumentException(String.format("'%1$s' is not a directory.", testDataDir)); - } - } - } - - public String[] input(final String fileName) throws Exception { - Path xmlPath = inputPath.resolve(fileName); - return new String[]{read(xmlPath), xmlPath.toString()}; - } - - public String expected(final String fileName) { - Path xmlPath = expectedPath.resolve(fileName); - return read(xmlPath); - } - - private String read(final Path xmlPath) { - if (!Files.isRegularFile(xmlPath)) { - throw new IllegalArgumentException(String.format("'%1$s' is not a regular file.", xmlPath)); - } - try { - String checkedOutFileContent = new String(java.nio.file.Files.readAllBytes(xmlPath), "UTF8"); - return checkedOutFileContent.replace("\r", ""); //Align GIT end-of-line normalization - } catch (IOException e) { - throw new IllegalArgumentException(String.format("Failed to read '%1$s'.", xmlPath), e); - } - } - - public Path getRestrictionsPath(String fileName) { - Path filePath = restrictionsPath.resolve(fileName); - if (!Files.exists(filePath)) { - throw new IllegalArgumentException(String.format("'%1$s' is not a restrictions file.", fileName)); - } - return filePath; - } -} diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/css.html b/_ext/eclipse-wtp/src/test/resources/html/expected/css.html deleted file mode 100644 index cfaea67cd0..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/css.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/html4.html b/_ext/eclipse-wtp/src/test/resources/html/expected/html4.html deleted file mode 100644 index a344ed08a0..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/html4.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Before -
After - -
- - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/html5.html b/_ext/eclipse-wtp/src/test/resources/html/expected/html5.html deleted file mode 100644 index c79dfdce62..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/html5.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Before -
After - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/invalid_syntax.html b/_ext/eclipse-wtp/src/test/resources/html/expected/invalid_syntax.html deleted file mode 100644 index 9c05557bb7..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/invalid_syntax.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - -T<WHATSOEVER></WHATSOEVER> - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/javascript.html b/_ext/eclipse-wtp/src/test/resources/html/expected/javascript.html deleted file mode 100644 index a33ea60503..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/javascript.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/expected/utf-8.html b/_ext/eclipse-wtp/src/test/resources/html/expected/utf-8.html deleted file mode 100644 index 4bcd6a03c1..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/expected/utf-8.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - -ÄÜâ‚Ŧ - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/bom.html b/_ext/eclipse-wtp/src/test/resources/html/input/bom.html deleted file mode 100644 index 3fe6abe797..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/bom.html +++ /dev/null @@ -1,2 +0,0 @@ -īģŋ -ÄÜâ‚Ŧ \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/css.html b/_ext/eclipse-wtp/src/test/resources/html/input/css.html deleted file mode 100644 index 8ccbc4dcc8..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/css.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/html4.html b/_ext/eclipse-wtp/src/test/resources/html/input/html4.html deleted file mode 100644 index 45aa7b56f7..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/html4.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -Before -
-After - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/html5.html b/_ext/eclipse-wtp/src/test/resources/html/input/html5.html deleted file mode 100644 index 216c53e8e3..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/html5.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -Before -
-After - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/invalid_syntax.html b/_ext/eclipse-wtp/src/test/resources/html/input/invalid_syntax.html deleted file mode 100644 index 04c6cf7f2a..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/invalid_syntax.html +++ /dev/null @@ -1,4 +0,0 @@ - - - -T</whatsoever> \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/javascript.html b/_ext/eclipse-wtp/src/test/resources/html/input/javascript.html deleted file mode 100644 index dccde2c2dd..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/javascript.html +++ /dev/null @@ -1,34 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> -<script type="text/javascript">console.log("One line")</script> -<script type="text/javascript"> -console.log("Line break, no indent"); -</script> -<script type="text/javascript"> - console.log("Line break, wrong indent") -</script> -</head> -<body> -<nav> -<script type="text/javascript">console.log("One line, nested")</script> -<script type="text/javascript"> -console.log("Line break, no indent, nested") -</script> -<script type="text/javascript"> - console.log("Line break, wrong indent, nested"); -</script> - <script type="text/javascript"> -console.log("Wrong tag indents") - </script> -<script type="text/javascript"> -console.log("Empty lines when closing."); - - </script> -<script>console.log("Script without type")</script> -<script type="text/javascript"></script> -<script type="text/javascript"> if (condition) { console.log("Missing end of JS block")</script> -</nav> -</body> -</html> \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/input/utf-8.html b/_ext/eclipse-wtp/src/test/resources/html/input/utf-8.html deleted file mode 100644 index abf1a7d9a7..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/input/utf-8.html +++ /dev/null @@ -1,2 +0,0 @@ -<!DOCTYPE html> -<html><head><meta charset="UTF-8"><title>ÄÜâ‚Ŧ \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/html/restrictions/ReadMe.txt b/_ext/eclipse-wtp/src/test/resources/html/restrictions/ReadMe.txt deleted file mode 100644 index 37c58641d8..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/html/restrictions/ReadMe.txt +++ /dev/null @@ -1,9 +0,0 @@ -Various manual tests have been performed with external and internal DTDs. -They seem not to be used by the HTML formatter. Instead they are a -helper to parse the DOCTYPE and select the right content model. -The DTDParser.parse throws a SAXParseException when parsing the -HTML DTDs provided with the WST JARs (accessed by the System catalog). -This nominal and expected exception is eaten (DTDParser.currentDTD not set). - -Since currently there seems to be no use case for DTD/XSD/catalog, -we omit it for Spotless integration. \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_external.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_external.test deleted file mode 100644 index 6f383afaa7..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_external.test +++ /dev/null @@ -1,8 +0,0 @@ - - - - indent - this text - preserve - spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_relative.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_relative.test deleted file mode 100644 index 37c565592b..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/dtd_relative.test +++ /dev/null @@ -1,8 +0,0 @@ - - - - indent - this text - preserve - spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/xml_space.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/xml_space.test deleted file mode 100644 index 8bc506f692..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/xml_space.test +++ /dev/null @@ -1,4 +0,0 @@ - - foo bar - preserve space - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_external.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_external.test deleted file mode 100644 index 0456632618..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_external.test +++ /dev/null @@ -1,7 +0,0 @@ - - - remove spaces - preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_not_found.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_not_found.test deleted file mode 100644 index 78a27d902c..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_not_found.test +++ /dev/null @@ -1,7 +0,0 @@ - - - remove spaces - preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_relative.test b/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_relative.test deleted file mode 100644 index 93e86ab38e..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/expected/xsd_relative.test +++ /dev/null @@ -1,7 +0,0 @@ - - - remove spaces - preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_external.test b/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_external.test deleted file mode 100644 index 1293b55e18..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_external.test +++ /dev/null @@ -1,8 +0,0 @@ - - - - indent - this text - preserve - spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_relative.test b/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_relative.test deleted file mode 100644 index 431354868c..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/dtd_relative.test +++ /dev/null @@ -1,8 +0,0 @@ - - - - indent - this text - preserve - spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/xml_space.test b/_ext/eclipse-wtp/src/test/resources/xml/input/xml_space.test deleted file mode 100644 index 2ddd865f1c..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/xml_space.test +++ /dev/null @@ -1 +0,0 @@ - foo bar preserve space \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_external.test b/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_external.test deleted file mode 100644 index e75b7ca06f..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_external.test +++ /dev/null @@ -1,4 +0,0 @@ - - - remove spaces preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_not_found.test b/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_not_found.test deleted file mode 100644 index 483be84785..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_not_found.test +++ /dev/null @@ -1,4 +0,0 @@ - - - remove spaces preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_relative.test b/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_relative.test deleted file mode 100644 index ce8392adf8..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/input/xsd_relative.test +++ /dev/null @@ -1,4 +0,0 @@ - - - remove spaces preserve spaces - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/catalog.xml b/_ext/eclipse-wtp/src/test/resources/xml/restrictions/catalog.xml deleted file mode 100644 index 2fba8dc2b9..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/catalog.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.dtd b/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.dtd deleted file mode 100644 index 5076fd91e0..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.dtd +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.xsd b/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.xsd deleted file mode 100644 index 6b0e3b5e00..0000000000 --- a/_ext/eclipse-wtp/src/test/resources/xml/restrictions/test.xsd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/_ext/gradle/java-setup.gradle b/_ext/gradle/java-setup.gradle deleted file mode 100644 index 598077d73c..0000000000 --- a/_ext/gradle/java-setup.gradle +++ /dev/null @@ -1,25 +0,0 @@ -////////// -// JAVA // -////////// -apply from: rootProject.file('gradle/changelog.gradle') -version = spotlessChangelog.versionNext -apply from: rootProject.file('gradle/java-setup.gradle') - -// Allow declaration of api/compile dependencies -apply plugin: 'java-library' - -// Show warning locations, fail on warnings -tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" - options.compilerArgs << "-Xlint:deprecation" - options.compilerArgs << "-Werror" -} - -//Currently testlib is not used by _ext. Hence the external dependencies are added here. -dependencies { - testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" - testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" - testImplementation project(':testlib') -} - -test { useJUnitPlatform() } diff --git a/_ext/gradle/p2-fat-jar-setup.gradle b/_ext/gradle/p2-fat-jar-setup.gradle deleted file mode 100644 index a8f4e242af..0000000000 --- a/_ext/gradle/p2-fat-jar-setup.gradle +++ /dev/null @@ -1,165 +0,0 @@ -apply from: rootProject.file('_ext/gradle/java-setup.gradle') - -apply plugin: 'com.diffplug.p2.asmaven' - -ext { - // P2 Repository URL - if (!project.hasProperty('p2Repository')) { - p2Repository = "p2Repository not defined in project" - } - // P2 dependencies - if (!project.hasProperty('p2Dependencies')) { - p2Dependencies = "p2Dependencies not defined in project" - } - - // Some JARs may include JARs themselfs, which shall be unpacked and added to the embedded class folder. - if (!project.hasProperty('internalJars')) { - internalJars = [] - } - - // Include form the JARs, which goes into a fat-jar with the spottless formatter interface. - if (!project.hasProperty('jarInclude')) { - jarInclude = [ - '**/*.class', // Take all classes - '**/*.properties', // Text resources (for messages, etc) - '**/*.xml', // Plugin XML and other resources - '*.html', // License information about the included JARs, - 'META-INF/**' // Plugin manifest and addtional information - ] - } - - // Exclude form the JARs, which goes into a fat-jar with the spottless formatter interface. - if (!project.hasProperty('jarExclude')) { - jarExclude = [ - 'META-INF/*.RSA', // The eclipse jars are signed, and our fat-jar breaks the signatures - 'META-INF/*.SF', // ... so all signatures are filtered - ] - } - - // Map fat-JAR resources path if JAR file name does not correspond to plugin package name (e.g. required for internal plugins) - if (!project.hasProperty('fatJarResourcesMap')) { - fatJarResourcesMap = [:] - } - - - // The directory contains all external classes for the fat-jar - embeddedClassesDirName = 'build/embeddedClasses' - embeddedClassesDir = project.file(embeddedClassesDirName) -} - -if (gradle.startParameter.projectProperties.get('com.diffplug.spotless.include.ext.nop2') != 'true') { - // build a maven repo in our build folder containing these artifacts - p2AsMaven { - group 'p2', { - repo project.p2Repository - p2Dependencies.keySet.each { p2.addIU(it) } - p2ant { - /* - Define p2ant proxy settings as a closure. Refer to the API documents for instructions: - https://diffplug.github.io/goomph/javadoc/3.17.4/com/diffplug/gradle/p2/AsMavenPlugin.html - */ - if (project.hasProperty('setP2AntProxy')) { - setP2AntProxy(it) - } - } - } - } -} - -configurations { - embeddedJars // P2 JARs the fat-JAR is based uppon -} - -dependencies { - p2Dependencies.each { groupArtifact, version -> - embeddedJars "p2:${groupArtifact}:${version}" - } - // Includes the classes from P2 JARs during compilation - implementation files(embeddedClassesDir) -} - -jar { - // Add P2 clases to fat-JAR - from embeddedClassesDir -} - -////////////////////////// -// Unpack External Deps // -////////////////////////// -import java.io.File - -task unjarEmbeddedClasses { - description = "Copies filtered set of embedded classes from the Eclise/GrEclipse dependencies to '${project.relativePath(embeddedClassesDir)}'." - inputs.files(configurations.embeddedJars) - inputs.property('internalJars', internalJars) - inputs.property('jarInclude', jarInclude) - inputs.property('jarExclude', jarExclude) - inputs.property('fatJarResourcesMap', fatJarResourcesMap) - outputs.dir(embeddedClassesDir) - - doLast { - embeddedClassesDir.deleteDir() - embeddedClassesDir.mkdirs() - configurations.embeddedJars.each { - unjar(it, embeddedClassesDir) - } - // Unpack internal JARs. Maintain the order defined in internalJars - internalJars.each { - fileTree(embeddedClassesDir).include("${it}.jar").each { - unjar(it, embeddedClassesDir) - delete(it) - } - } - } -} - -def unjar(File jarFile, File destDir) { - ant.unjar(src: jarFile, dest: destDir) { - patternset { - jarInclude.each { - include(name: "${it}") - } - internalJars.each { - include(name: "**/${it}.jar") - } - jarExclude.each { - exclude(name: "${it}") - } - } - } - // Provide Fat JAR resources (following naming convention of spotless-eclipse-base) - def fat_jar_resource_dir = jarFile.getName().split('-')[0] - fat_jar_resource_dir = fatJarResourcesMap.getOrDefault(fat_jar_resource_dir, fat_jar_resource_dir) - ant.move(todir: "${destDir}/${fat_jar_resource_dir}/META-INF", quiet: 'true', failonerror: 'false') { - fileset(dir: "${destDir}/META-INF") - } - //Keep licenses and other human readable information for transparency - ant.move(todir: "${destDir}/${fat_jar_resource_dir}", quiet: 'true') { - fileset(dir: destDir) { - include(name: '*') - type(type: 'file') - exclude(name: '*jar-*') - exclude(name: '*.jar') - } - } - -} - -tasks.compileJava.dependsOn(unjarEmbeddedClasses) - -///////// -// IDE // -///////// - -apply plugin: 'eclipse' - -// always create fresh projects -tasks.eclipse.dependsOn(cleanEclipse) -// Encure that the dependent classes are preovided for compilation if project is build via Eclipse instead of command line -tasks.eclipseClasspath.dependsOn(unjarEmbeddedClasses) - -apply plugin: 'idea' - -// Encure that the dependent classes are preovided for compilation if project is build via Eclipse instead of command line -tasks.idea.dependsOn(unjarEmbeddedClasses) - diff --git a/_ext/gradle/update-lockfile.gradle b/_ext/gradle/update-lockfile.gradle deleted file mode 100644 index 098d960eff..0000000000 --- a/_ext/gradle/update-lockfile.gradle +++ /dev/null @@ -1,6 +0,0 @@ -// Use file locking -configurations.all { - resolutionStrategy { - activateDependencyLocking() - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7c3a3dede9..fafd6abe43 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,14 @@ +apply plugin: 'dev.equo.ide' +equoIde { + branding().title('Spotless').icon(file('_images/spotless_logo.png')) + welcome().openUrl('https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md') + gradleBuildship().autoImport('.') +} + +repositories { + mavenCentral() +} + apply from: rootProject.file('gradle/java-publish.gradle') apply from: rootProject.file('gradle/changelog.gradle') allprojects { @@ -5,21 +16,14 @@ allprojects { } apply from: rootProject.file('gradle/spotless-freshmark.gradle') -repositories { - mavenCentral() -} spotless { groovyGradle { target '*.gradle', 'gradle/*.gradle' } format 'dotfiles', { target '.gitignore', '.gitattributes', '.editorconfig' - indentWithSpaces(2) + leadingTabsToSpaces(2) trimTrailingWhitespace() endWithNewline() } } - -static Class spotBugsTaskType() { - return com.github.spotbugs.snom.SpotBugsTask -} diff --git a/gradle.properties b/gradle.properties index fcad5c5ed4..a6d9cd7322 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,9 @@ # To fix metaspace errors -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + name=spotless description=Spotless - keep your code spotless with Gradle org=diffplug @@ -16,8 +20,7 @@ artifactIdMaven=spotless-maven-plugin artifactIdGradle=spotless-plugin-gradle # Build requirements -VER_JAVA=1.8 -VER_SPOTBUGS=4.5.0 +VER_JAVA=11 VER_JSR_305=3.0.2 # Dependencies provided by Spotless plugin @@ -25,13 +28,8 @@ VER_SLF4J=[1.6,2.0[ # Used in multiple places VER_DURIAN=1.2.0 -VER_JGIT=5.13.0.202109080827-r -VER_JUNIT=5.8.0 -VER_ASSERTJ=3.15.0 -VER_MOCKITO=3.3.3 - -# Used for Maven Plugin -VER_MAVEN_API=3.0 -VER_ECLIPSE_AETHER=0.9.0.M2 -VER_MUSTACHE=0.9.6 -VER_PLEXUS_RESOURCES=1.2.0 +VER_JGIT=6.10.0.202406032230-r +VER_JUNIT=5.12.2 +VER_ASSERTJ=3.27.3 +VER_MOCKITO=5.17.0 +VER_SELFIE=2.4.2 \ No newline at end of file diff --git a/gradle/build-scans.gradle b/gradle/build-scans.gradle deleted file mode 100644 index b134b6f142..0000000000 --- a/gradle/build-scans.gradle +++ /dev/null @@ -1,40 +0,0 @@ -File acceptFile = new File(gradle.gradleUserHomeDir, "build-scans/spotless/gradle-scans-license-agree.txt") -acceptFile.parentFile.mkdirs() -def env = System.getenv() -boolean isCI = env.CI || env.TRAVIS -boolean hasAccepted = isCI || env.SPOTLESS_GRADLE_SCANS_ACCEPT=='yes' || acceptFile.exists() && acceptFile.text.trim() == 'yes' -boolean hasRefused = env.SPOTLESS_GRADLE_SCANS_ACCEPT=='no' || acceptFile.exists() && acceptFile.text.trim() == 'no' - -buildScan { - if (hasAccepted) { - termsOfServiceAgree = 'yes' - } else if (!hasRefused) { - gradle.buildFinished { - println """ -This build uses Gradle Build Scans to gather statistics, share information about -failures, environmental issues, dependencies resolved during the build and more. -Build scans will be published after each build, if you accept the terms of -service, and in particular the privacy policy. - -Please read - - https://gradle.com/terms-of-service - https://gradle.com/legal/privacy - -and then: - - - set the `SPOTLESS_GRADLE_SCANS_ACCEPT` to `yes`/`no` if you agree with/refuse the TOS - - or create the ${acceptFile} file with `yes`/`no` in it if you agree with/refuse - - echo 'yes' >> ${acceptFile} - - or - - echo 'no' >> ${acceptFile} - -And we'll not bother you again. Note that build scans are only made public if -you share the URL at the end of the build. -""" - } - } -} diff --git a/gradle/changelog.gradle b/gradle/changelog.gradle index 316e98a2d7..994e3558dc 100644 --- a/gradle/changelog.gradle +++ b/gradle/changelog.gradle @@ -1,13 +1,15 @@ String kind +String releaseTitle if (project.name == 'plugin-gradle') { kind = 'gradle' + releaseTitle = 'Gradle Plugin' } else if (project.name == 'plugin-maven') { kind = 'maven' -} else if (project.name.startsWith('eclipse')) { - kind = 'ext-' + project.name + releaseTitle = 'Maven Plugin' } else { assert project == rootProject kind = 'lib' + releaseTitle = 'Lib' } // the root project and plugins have their own changelogs @@ -17,14 +19,11 @@ spotlessChangelog { // need -Prelease=true in order to do a publish appendDashSnapshotUnless_dashPrelease=true + branch 'release' tagPrefix "${kind}/" commitMessage "Published ${kind}/{{version}}" // {{version}} will be replaced -} - -if (gradle.startParameter.projectProperties.get('com.diffplug.spotless.include.ext.nop2') == 'true') { - tasks.named('changelogPrint') { - enabled = kind.startsWith('ext-') - } + tagMessage "{{changes}}" + runAfterPush "gh release create ${kind}/{{version}} --title '${releaseTitle} v{{version}}' --notes-from-tag" } if (project == rootProject) { diff --git a/gradle/java-publish.gradle b/gradle/java-publish.gradle index 78e73a351f..99879e6b2e 100644 --- a/gradle/java-publish.gradle +++ b/gradle/java-publish.gradle @@ -1,6 +1,20 @@ +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 { @@ -9,7 +23,7 @@ if (project.parent == null) { nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) username = System.env['ORG_GRADLE_PROJECT_nexus_user'] - password = System.env['ORG_GRADLE_PROJECT_nexus_pass'] + password = decode64('ORG_GRADLE_PROJECT_nexus_pass64') } } } @@ -63,12 +77,11 @@ javadoc { // // Thus, no javadoc warnings. options.addStringOption('Xdoclint:none', '-quiet') - options.addStringOption('source', '8') + options.addStringOption('source', '11') // setup the header options.header javadocInfo // setup links - options.linksOffline('https://docs.oracle.com/javase/8/docs/api/', "${dotdotGradle}/javadoc/java8") - options.linksOffline('https://docs.gradle.org/2.14/javadoc/', "${dotdotGradle}/javadoc/gradle") + 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") @@ -94,17 +107,22 @@ model { 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 project.ext.artifactId - description project.description - url "https://github.com/${project.org}/${rootProject.name}" + name projectExtArtifactId + description projectDescription + url "https://github.com/${projectOrg}/${rootProjectName}" scm { - url "https://github.com/${project.org}/${rootProject.name}" - connection "scm:git:https://github.com/${project.org}/${rootProject.name}.git" - developerConnection "scm:git:ssh:git@github.com/${project.org}/${rootProject.name}.git" + 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) { @@ -156,9 +174,13 @@ model { } } -if (!version.endsWith('-SNAPSHOT')) { +if (System.env['JITPACK'] == 'true' || version.endsWith('-SNAPSHOT')) { signing { - String gpg_key = new String(System.env['ORG_GRADLE_PROJECT_gpg_key64'].decodeBase64()) + setRequired(false) + } +} else { + signing { + String gpg_key = decode64('ORG_GRADLE_PROJECT_gpg_key64') useInMemoryPgpKeys(gpg_key, System.env['ORG_GRADLE_PROJECT_gpg_passphrase']) sign(publishing.publications) } @@ -172,14 +194,12 @@ if (!version.endsWith('-SNAPSHOT')) { } // ensures that changelog bump and push only happens if the publish was successful def thisProj = project - afterEvaluate { - changelogTasks.named('changelogBump').configure { - dependsOn thisProj.tasks.named('publishPluginMavenPublicationToSonatypeRepository') - dependsOn rootProject.tasks.named('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') - } + 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') } } } diff --git a/gradle/java-setup.gradle b/gradle/java-setup.gradle index 5d9d756414..6663447ba4 100644 --- a/gradle/java-setup.gradle +++ b/gradle/java-setup.gradle @@ -1,39 +1,43 @@ ////////// // JAVA // ////////// -repositories { mavenCentral() } // setup java apply plugin: 'java' - -sourceCompatibility = VER_JAVA -targetCompatibility = VER_JAVA -tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = Integer.parseInt(VER_JAVA) +} ////////////// // SPOTBUGS // ////////////// apply plugin: 'com.github.spotbugs' spotbugs { - toolVersion = VER_SPOTBUGS ignoreFailures = false // bug free or it doesn't ship! - reportsDir = file('build/spotbugs') - reportLevel = 'medium' // low|medium|high (low = sensitive to even minor mistakes) + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = com.github.spotbugs.snom.Confidence.valueOf('MEDIUM') omitVisitors = [ - 'FindReturnRef'] // https://spotbugs.readthedocs.io/en/latest/detectors.html#findreturnref + // 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.named('spotbugsMain') { - // only run on Java 8 (no benefit to running twice) - enabled = org.gradle.api.JavaVersion.current() == org.gradle.api.JavaVersion.VERSION_11 + +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 'net.jcip:jcip-annotations:1.0' - compileOnly "com.github.spotbugs:spotbugs-annotations:${VER_SPOTBUGS}" + compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" compileOnly "com.google.code.findbugs:jsr305:${VER_JSR_305}" } diff --git a/gradle/javadoc/java8/package-list b/gradle/javadoc/java8/package-list deleted file mode 100644 index 351c186855..0000000000 --- a/gradle/javadoc/java8/package-list +++ /dev/null @@ -1,217 +0,0 @@ -java.applet -java.awt -java.awt.color -java.awt.datatransfer -java.awt.dnd -java.awt.event -java.awt.font -java.awt.geom -java.awt.im -java.awt.im.spi -java.awt.image -java.awt.image.renderable -java.awt.print -java.beans -java.beans.beancontext -java.io -java.lang -java.lang.annotation -java.lang.instrument -java.lang.invoke -java.lang.management -java.lang.ref -java.lang.reflect -java.math -java.net -java.nio -java.nio.channels -java.nio.channels.spi -java.nio.charset -java.nio.charset.spi -java.nio.file -java.nio.file.attribute -java.nio.file.spi -java.rmi -java.rmi.activation -java.rmi.dgc -java.rmi.registry -java.rmi.server -java.security -java.security.acl -java.security.cert -java.security.interfaces -java.security.spec -java.sql -java.text -java.text.spi -java.time -java.time.chrono -java.time.format -java.time.temporal -java.time.zone -java.util -java.util.concurrent -java.util.concurrent.atomic -java.util.concurrent.locks -java.util.function -java.util.jar -java.util.logging -java.util.prefs -java.util.regex -java.util.spi -java.util.stream -java.util.zip -javax.accessibility -javax.activation -javax.activity -javax.annotation -javax.annotation.processing -javax.crypto -javax.crypto.interfaces -javax.crypto.spec -javax.imageio -javax.imageio.event -javax.imageio.metadata -javax.imageio.plugins.bmp -javax.imageio.plugins.jpeg -javax.imageio.spi -javax.imageio.stream -javax.jws -javax.jws.soap -javax.lang.model -javax.lang.model.element -javax.lang.model.type -javax.lang.model.util -javax.management -javax.management.loading -javax.management.modelmbean -javax.management.monitor -javax.management.openmbean -javax.management.relation -javax.management.remote -javax.management.remote.rmi -javax.management.timer -javax.naming -javax.naming.directory -javax.naming.event -javax.naming.ldap -javax.naming.spi -javax.net -javax.net.ssl -javax.print -javax.print.attribute -javax.print.attribute.standard -javax.print.event -javax.rmi -javax.rmi.CORBA -javax.rmi.ssl -javax.script -javax.security.auth -javax.security.auth.callback -javax.security.auth.kerberos -javax.security.auth.login -javax.security.auth.spi -javax.security.auth.x500 -javax.security.cert -javax.security.sasl -javax.sound.midi -javax.sound.midi.spi -javax.sound.sampled -javax.sound.sampled.spi -javax.sql -javax.sql.rowset -javax.sql.rowset.serial -javax.sql.rowset.spi -javax.swing -javax.swing.border -javax.swing.colorchooser -javax.swing.event -javax.swing.filechooser -javax.swing.plaf -javax.swing.plaf.basic -javax.swing.plaf.metal -javax.swing.plaf.multi -javax.swing.plaf.nimbus -javax.swing.plaf.synth -javax.swing.table -javax.swing.text -javax.swing.text.html -javax.swing.text.html.parser -javax.swing.text.rtf -javax.swing.tree -javax.swing.undo -javax.tools -javax.transaction -javax.transaction.xa -javax.xml -javax.xml.bind -javax.xml.bind.annotation -javax.xml.bind.annotation.adapters -javax.xml.bind.attachment -javax.xml.bind.helpers -javax.xml.bind.util -javax.xml.crypto -javax.xml.crypto.dom -javax.xml.crypto.dsig -javax.xml.crypto.dsig.dom -javax.xml.crypto.dsig.keyinfo -javax.xml.crypto.dsig.spec -javax.xml.datatype -javax.xml.namespace -javax.xml.parsers -javax.xml.soap -javax.xml.stream -javax.xml.stream.events -javax.xml.stream.util -javax.xml.transform -javax.xml.transform.dom -javax.xml.transform.sax -javax.xml.transform.stax -javax.xml.transform.stream -javax.xml.validation -javax.xml.ws -javax.xml.ws.handler -javax.xml.ws.handler.soap -javax.xml.ws.http -javax.xml.ws.soap -javax.xml.ws.spi -javax.xml.ws.spi.http -javax.xml.ws.wsaddressing -javax.xml.xpath -org.ietf.jgss -org.omg.CORBA -org.omg.CORBA.DynAnyPackage -org.omg.CORBA.ORBPackage -org.omg.CORBA.TypeCodePackage -org.omg.CORBA.portable -org.omg.CORBA_2_3 -org.omg.CORBA_2_3.portable -org.omg.CosNaming -org.omg.CosNaming.NamingContextExtPackage -org.omg.CosNaming.NamingContextPackage -org.omg.Dynamic -org.omg.DynamicAny -org.omg.DynamicAny.DynAnyFactoryPackage -org.omg.DynamicAny.DynAnyPackage -org.omg.IOP -org.omg.IOP.CodecFactoryPackage -org.omg.IOP.CodecPackage -org.omg.Messaging -org.omg.PortableInterceptor -org.omg.PortableInterceptor.ORBInitInfoPackage -org.omg.PortableServer -org.omg.PortableServer.CurrentPackage -org.omg.PortableServer.POAManagerPackage -org.omg.PortableServer.POAPackage -org.omg.PortableServer.ServantLocatorPackage -org.omg.PortableServer.portable -org.omg.SendingContext -org.omg.stub.java.rmi -org.w3c.dom -org.w3c.dom.bootstrap -org.w3c.dom.events -org.w3c.dom.ls -org.w3c.dom.views -org.xml.sax -org.xml.sax.ext -org.xml.sax.helpers diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle index c435bc7e81..650c04b84b 100644 --- a/gradle/special-tests.gradle +++ b/gradle/special-tests.gradle @@ -1,27 +1,41 @@ -apply plugin: 'org.gradle.test-retry' apply plugin: 'com.adarshr.test-logger' + +// See com.diffplug.spotless.tag package for available JUnit 5 @Tag annotations def special = [ - 'Npm', - 'Black', - 'Clang' + 'black', + 'buf', + 'clang', + 'gofmt', + 'npm', + 'shfmt' ] boolean isCiServer = System.getenv().containsKey("CI") -tasks.named('test') { - // See com.diffplug.spotless.tag package for available JUnit 5 @Tag annotations - useJUnitPlatform { - excludeTags special as String[] - } +tasks.withType(Test).configureEach { if (isCiServer) { retry { 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}", Test) { + tasks.register("test${tag.capitalize()}", Test) { useJUnitPlatform { includeTags tag } } } diff --git a/gradle/spotless-freshmark.gradle b/gradle/spotless-freshmark.gradle index 6df47581d4..ddaedcfa3e 100644 --- a/gradle/spotless-freshmark.gradle +++ b/gradle/spotless-freshmark.gradle @@ -1,30 +1,4 @@ -import java.util.regex.Matcher -import java.util.regex.Pattern - - -def thisVm() { - String jre = System.getProperty("java.version") - if (jre.startsWith("1.8")) { - return 8 - } else { - Matcher matcher = Pattern.compile("(\\d+)").matcher(jre) - if (!matcher.find()) { - throw new IllegalArgumentException("Expected " + jre + " to start with an integer") - } - int version = Integer.parseInt(matcher.group(1)) - if (version <= 8) { - throw new IllegalArgumentException("Expected " + jre + " to start with an integer greater than 8") - } - return version - } -} - -if (thisVm() >= 15) { - // freshmark doesn't run on JRE 15+ - return -} - apply plugin: 'com.diffplug.spotless' import com.diffplug.gradle.spotless.FreshMarkExtension @@ -36,8 +10,7 @@ Action freshmarkSetup = { it.put('yes', ':+1:') it.put('no', ':white_large_square:') } - it.indentWithSpaces(2) - it.trimTrailingWhitespace() + it.leadingTabsToSpaces(2) it.endWithNewline() } @@ -75,7 +48,7 @@ if (tasks.names.contains('changelogCheck')) { changelogBumpFreshmark.dependsOn tasks.named('changelogBump') def changelogBumpFreshmarkGitAdd = tasks.register('changelogBumpFreshmarkGitAdd') { - // this git add add should run after the freshmark + // this git add should run after the freshmark dependsOn(changelogBumpFreshmark) // do the git add doLast { diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 96037b21a3..677630ac69 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -22,6 +22,7 @@ spotless { eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml') trimTrailingWhitespace() removeUnusedImports() + // TODO: formatAnnotations() custom 'noInternalDeps', noInternalDepsClosure } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar old mode 100755 new mode 100644 index e708b1c023..9bbc975c74 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 e750102e09..37f853b1c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c81..faf93008b7 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright Š 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,69 +15,103 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ÂĢ$varÂģ, ÂĢ${var}Âģ, ÂĢ${var:-default}Âģ, ÂĢ${var+SET}Âģ, +# ÂĢ${var#prefix}Âģ, ÂĢ${var%suffix}Âģ, and ÂĢ$( cmd )Âģ; +# * compound commands having a testable exit status, especially ÂĢcaseÂģ; +# * various built-in commands including ÂĢcommandÂģ, ÂĢsetÂģ, and ÂĢulimitÂģ. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/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/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c4..9d21a21834 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000000..adb3fe10c8 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle index 1f6fdd8962..880830740b 100644 --- a/lib-extra/build.gradle +++ b/lib-extra/build.gradle @@ -1,30 +1,105 @@ 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') +String VER_SOLSTICE = '1.8.1' dependencies { - api project(':lib') + api projects.lib // misc useful utilities implementation "com.diffplug.durian:durian-core:${VER_DURIAN}" implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" // needed by GitAttributesLineEndings implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}" implementation "com.googlecode.concurrent-trees:concurrent-trees:2.6.1" - // used for xml parsing in EclipseFormatter - implementation "org.codehaus.groovy:groovy-xml:3.0.9" + // for eclipse + implementation "dev.equo.ide:solstice:${VER_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 "org.eclipse.platform:org.eclipse.osgi:3.23.0" // testing - testImplementation project(':testlib') + testImplementation projects.testlib testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" +} +spotless { + java { + replaceRegex 'enforceSolsticeVersion', '"dev.equo.ide:solstice:(.*)"', '"dev.equo.ide:solstice:' + VER_SOLSTICE + '"' + } } -// we'll hold the core lib to a high standard -spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes) +apply from: rootProject.file('gradle/special-tests.gradle') +tasks.withType(Test).configureEach { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) { + // needed for EclipseCdtFormatterStepTest + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' + } +} + +def NEEDS_P2_DEPS = [ + // (alphabetic order please) + 'cdt', + 'groovy', + 'jdt' +] +if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + NEEDS_P2_DEPS.remove('cdt') +} +for (needsP2 in NEEDS_P2_DEPS) { + sourceSets.register(needsP2) { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + java {} + } + dependencies { + add("${needsP2}CompileOnly", "dev.equo.ide:solstice:${VER_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 +} -test { useJUnitPlatform() } +apply plugin: 'dev.equo.p2deps' +p2deps { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + 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') +} diff --git a/_ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImpl.java b/lib-extra/src/cdt/java/com/diffplug/spotless/extra/glue/cdt/EclipseCdtFormatterStepImpl.java similarity index 66% rename from _ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImpl.java rename to lib-extra/src/cdt/java/com/diffplug/spotless/extra/glue/cdt/EclipseCdtFormatterStepImpl.java index 4482f3e752..045860f535 100644 --- a/_ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/EclipseCdtFormatterStepImpl.java +++ b/lib-extra/src/cdt/java/com/diffplug/spotless/extra/glue/cdt/EclipseCdtFormatterStepImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.extra.eclipse.cdt; +package com.diffplug.spotless.extra.glue.cdt; import java.util.Map; import java.util.Map.Entry; @@ -21,23 +21,16 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.formatter.CodeFormatter; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; - /** Formatter step which calls out to the Eclipse CDT formatter. */ public class EclipseCdtFormatterStepImpl { private final CodeFormatter codeFormatter; public EclipseCdtFormatterStepImpl(Properties settings) throws Exception { - SpotlessEclipseFramework.setup(new FrameworkConfig()); Stream> stream = settings.entrySet().stream(); Map settingsMap = stream.collect(Collectors.toMap( e -> String.valueOf(e.getKey()), @@ -45,24 +38,10 @@ public EclipseCdtFormatterStepImpl(Properties settings) throws Exception { codeFormatter = org.eclipse.cdt.core.ToolFactory.createDefaultCodeFormatter(settingsMap); } - private static class FrameworkConfig implements SpotlessEclipseConfig { - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(EclipseCdtFormatterStepImpl.class.getPackage().getName()); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - config.add(new CCorePlugin()); - } - } - /** Formatting C/C++ string */ public String format(String raw) throws Exception { //The 'kind' can be set to CodeFormatter.K_UNKNOWN, since it is anyway ignored by the internal formatter - TextEdit edit = codeFormatter.format(CodeFormatter.K_UNKNOWN, raw, 0, raw.length(), 0, SpotlessEclipseFramework.LINE_DELIMITER); + TextEdit edit = codeFormatter.format(CodeFormatter.K_UNKNOWN, raw, 0, raw.length(), 0, "\n"); if (edit == null) { throw new IllegalArgumentException("Invalid C/C++ syntax for formatting."); } else { diff --git a/_ext/eclipse-groovy/src/main/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImpl.java b/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java similarity index 74% rename from _ext/eclipse-groovy/src/main/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImpl.java rename to lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java index 1d2b71c69f..163816c1a4 100644 --- a/_ext/eclipse-groovy/src/main/java/com/diffplug/spotless/extra/eclipse/groovy/GrEclipseFormatterStepImpl.java +++ b/lib-extra/src/groovy/java/com/diffplug/spotless/extra/glue/groovy/GrEclipseFormatterStepImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.extra.eclipse.groovy; +package com.diffplug.spotless.extra.glue.groovy; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Properties; import org.codehaus.groovy.eclipse.GroovyLogManager; @@ -37,15 +39,36 @@ import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.TextSelection; +import org.eclipse.osgi.internal.location.EquinoxLocations; import org.eclipse.text.edits.TextEdit; +import org.osgi.framework.Constants; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; +import dev.equo.solstice.NestedJars; +import dev.equo.solstice.ShimIdeBootstrapServices; +import dev.equo.solstice.Solstice; +import dev.equo.solstice.p2.CacheLocations; /** Spotless-Formatter step which calls out to the Groovy-Eclipse formatter. */ public class GrEclipseFormatterStepImpl { + static { + NestedJars.setToWarnOnly(); + NestedJars.onClassPath().confirmAllNestedJarsArePresentOnClasspath(CacheLocations.p2nestedJars()); + try { + var solstice = Solstice.findBundlesOnClasspath(); + solstice.warnAndModifyManifestsToFix(); + 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()); + solstice.openShim(props); + ShimIdeBootstrapServices.apply(props, solstice.getContext()); + solstice.start("org.apache.felix.scr"); + solstice.startAllWithLazy(false); + solstice.start("org.codehaus.groovy.eclipse.core"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Groovy compiler problems can be ignored. *

@@ -58,25 +81,11 @@ public class GrEclipseFormatterStepImpl { private final boolean ignoreFormatterProblems; public GrEclipseFormatterStepImpl(final Properties properties) throws Exception { - SpotlessEclipseFramework.setup(new FrameworkConfig()); PreferenceStore preferences = createPreferences(properties); preferencesStore = new FormatterPreferencesOnStore(preferences); ignoreFormatterProblems = Boolean.parseBoolean(properties.getProperty(IGNORE_FORMATTER_PROBLEMS, "false")); } - private static class FrameworkConfig implements SpotlessEclipseConfig { - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(GrEclipseFormatterStepImpl.class.getPackage().getName()); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.add(new GroovyCoreActivator()); - } - } - /** Formatting Groovy string */ public String format(String raw) throws Exception { IDocument doc = new Document(raw); @@ -94,32 +103,31 @@ public String format(String raw) throws Exception { /** * Eclipse Groovy formatter does not signal problems by its return value, but by logging errors. */ - private static class GroovyErrorListener implements ILogListener, IGroovyLogger { - - private final List errors; + private static final class GroovyErrorListener implements ILogListener, IGroovyLogger { + private final List errors; public GroovyErrorListener() { /* * We need a synchronized list here, in case multiple instantiations * run in parallel. */ - errors = Collections.synchronizedList(new ArrayList()); + errors = Collections.synchronizedList(new ArrayList<>()); ILog groovyLogger = GroovyCoreActivator.getDefault().getLog(); groovyLogger.addLogListener(this); - synchronized(GroovyLogManager.manager) { + synchronized (GroovyLogManager.manager) { GroovyLogManager.manager.addLogger(this); } } @Override public void logging(final IStatus status, final String plugin) { - errors.add(status.getMessage()); + errors.add(status.getException()); } public boolean errorsDetected() { ILog groovyLogger = GroovyCoreActivator.getDefault().getLog(); groovyLogger.removeLogListener(this); - synchronized(GroovyLogManager.manager) { + synchronized (GroovyLogManager.manager) { GroovyLogManager.manager.removeLogger(this); } return 0 != errors.size(); @@ -133,9 +141,10 @@ public String toString() { } else if (0 == errors.size()) { string.append("Step sucesfully executed."); } - for (String error : errors) { + for (Throwable error : errors) { + error.printStackTrace(); string.append(System.lineSeparator()); - string.append(error); + string.append(error.getMessage()); } return string.toString(); @@ -154,9 +163,12 @@ public boolean isCategoryEnabled(TraceCategory cat) { @Override public void log(TraceCategory arg0, String arg1) { - errors.add(arg1); + try { + throw new RuntimeException(arg1); + } catch (RuntimeException e) { + errors.add(e); + } } - } private static PreferenceStore createPreferences(final Properties properties) throws IOException { @@ -167,5 +179,4 @@ private static PreferenceStore createPreferences(final Properties properties) th preferences.load(input); return preferences; } - } diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/DefaultJavaElementComparator.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/DefaultJavaElementComparator.java new file mode 100644 index 0000000000..78791e240b --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/DefaultJavaElementComparator.java @@ -0,0 +1,389 @@ +/* + * Copyright 2024 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.glue.jdt; + +import java.util.Comparator; +import java.util.List; +import java.util.StringTokenizer; + +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.EnumConstantDeclaration; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.util.CompilationUnitSorter; +import org.eclipse.jdt.internal.core.dom.NaiveASTFlattener; + +/** + * This class is derived and adapted code from the Eclipse JDT project (Derivative Works according to EPL 2.0 license). + */ +@SuppressFBWarnings(value = "SE_COMPARATOR_SHOULD_BE_SERIALIZABLE", justification = "this comparator is not meant to be serialized") +class DefaultJavaElementComparator implements Comparator { + + static final int TYPE_INDEX = 0; + static final int CONSTRUCTORS_INDEX = 1; + static final int METHOD_INDEX = 2; + static final int FIELDS_INDEX = 3; + static final int INIT_INDEX = 4; + static final int STATIC_FIELDS_INDEX = 5; + static final int STATIC_INIT_INDEX = 6; + static final int STATIC_METHODS_INDEX = 7; + static final int ENUM_CONSTANTS_INDEX = 8; + static final int N_CATEGORIES = 9; + + static final int PUBLIC_INDEX = 0; + static final int PRIVATE_INDEX = 1; + static final int PROTECTED_INDEX = 2; + static final int DEFAULT_INDEX = 3; + static final int N_VISIBILITIES = 4; + + private final boolean doNotSortFields; + private final int[] memberCategoryOffsets; + private final boolean sortByVisibility; + private final int[] visibilityOffsets; + + static DefaultJavaElementComparator of( + boolean doNotSortFields, + String memberCategoryPreferences, + boolean sortByVisibility, + String visibilityPreferences) { + + int[] memberCategoryOffsets = new int[9]; + boolean success = fillMemberCategoryOffsets(memberCategoryPreferences, memberCategoryOffsets); + if (!success) { + String defaultValue = "T,SF,SI,SM,F,I,C,M"; + fillMemberCategoryOffsets(defaultValue, memberCategoryOffsets); + } + + int[] visibilityOffsets = new int[4]; + boolean success2 = fillVisibilityOffsets(visibilityPreferences, visibilityOffsets); + if (!success2) { + String defaultValue = "B,V,R,D"; + fillVisibilityOffsets(defaultValue, visibilityOffsets); + } + + return new DefaultJavaElementComparator(doNotSortFields, memberCategoryOffsets, sortByVisibility, visibilityOffsets); + } + + DefaultJavaElementComparator( + boolean doNotSortFields, + int[] memberCategoryOffsets, + boolean sortByVisibility, + int[] visibilityOffsets) { + + this.doNotSortFields = doNotSortFields; + this.memberCategoryOffsets = memberCategoryOffsets; + this.sortByVisibility = sortByVisibility; + this.visibilityOffsets = visibilityOffsets; + } + + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "we only accept valid tokens in the order string, otherwise we fall back to default value") + static boolean fillVisibilityOffsets(String preferencesString, int[] offsets) { + StringTokenizer tokenizer = new StringTokenizer(preferencesString, ","); + int i = 0; + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + switch (token) { + case "B": + offsets[PUBLIC_INDEX] = i++; + break; + case "D": + offsets[DEFAULT_INDEX] = i++; + break; + case "R": + offsets[PROTECTED_INDEX] = i++; + break; + case "V": + offsets[PRIVATE_INDEX] = i++; + } + } + return i == N_VISIBILITIES; + } + + @SuppressFBWarnings(value = "SF_SWITCH_NO_DEFAULT", justification = "we only accept valid tokens in the order string, otherwise we fall back to default value") + static boolean fillMemberCategoryOffsets(String orderString, int[] offsets) { + StringTokenizer tokenizer = new StringTokenizer(orderString, ","); + int i = 0; + offsets[8] = i++; + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + switch (token) { + case "C": + offsets[CONSTRUCTORS_INDEX] = i++; + break; + case "F": + offsets[FIELDS_INDEX] = i++; + break; + case "I": + offsets[INIT_INDEX] = i++; + break; + case "M": + offsets[METHOD_INDEX] = i++; + break; + case "T": + offsets[TYPE_INDEX] = i++; + break; + case "SF": + offsets[STATIC_FIELDS_INDEX] = i++; + break; + case "SI": + offsets[STATIC_INIT_INDEX] = i++; + break; + case "SM": + offsets[STATIC_METHODS_INDEX] = i++; + } + } + return i == N_CATEGORIES; + } + + private int category(BodyDeclaration bodyDeclaration) { + switch (bodyDeclaration.getNodeType()) { + case ASTNode.METHOD_DECLARATION: { + MethodDeclaration method = (MethodDeclaration) bodyDeclaration; + if (method.isConstructor()) { + return CONSTRUCTORS_INDEX; + } + int flags = method.getModifiers(); + if (Modifier.isStatic(flags)) + return STATIC_METHODS_INDEX; + else + return METHOD_INDEX; + } + case ASTNode.FIELD_DECLARATION: { + if (JdtFlags.isStatic(bodyDeclaration)) + return STATIC_FIELDS_INDEX; + else + return FIELDS_INDEX; + } + case ASTNode.INITIALIZER: { + int flags = bodyDeclaration.getModifiers(); + if (Modifier.isStatic(flags)) + return STATIC_INIT_INDEX; + else + return INIT_INDEX; + } + case ASTNode.TYPE_DECLARATION: + case ASTNode.ENUM_DECLARATION: + case ASTNode.ANNOTATION_TYPE_DECLARATION: + return TYPE_INDEX; + case ASTNode.ENUM_CONSTANT_DECLARATION: + return ENUM_CONSTANTS_INDEX; + case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION: + return METHOD_INDEX; // reusing the method index + + } + return 0; // should never happen + } + + private int getCategoryIndex(int category) { + return memberCategoryOffsets[category]; + } + + private int getVisibilityIndex(int modifierFlags) { + int kind = 3; + if (Flags.isPublic(modifierFlags)) { + kind = 0; + } else if (Flags.isProtected(modifierFlags)) { + kind = 2; + } else if (Flags.isPrivate(modifierFlags)) { + kind = 1; + } + return this.visibilityOffsets[kind]; + } + + /** + * This comparator follows the contract defined in CompilationUnitSorter.sort. + * + * @see Comparator#compare(java.lang.Object, java.lang.Object) + * @see CompilationUnitSorter#sort(int, org.eclipse.jdt.core.ICompilationUnit, int[], java.util.Comparator, int, org.eclipse.core.runtime.IProgressMonitor) + */ + @Override + @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", justification = "when switching to a more recent Java version, we can avoid those unconfirmed casts") + public int compare(BodyDeclaration bodyDeclaration1, BodyDeclaration bodyDeclaration2) { + boolean preserved1 = doNotSortFields && isSortPreserved(bodyDeclaration1); + boolean preserved2 = doNotSortFields && isSortPreserved(bodyDeclaration2); + + // Bug 407759: need to use a common category for all isSortPreserved members that are to be sorted in the same group: + int cat1 = category(bodyDeclaration1); + if (preserved1) { + cat1 = sortPreservedCategory(cat1); + } + int cat2 = category(bodyDeclaration2); + if (preserved2) { + cat2 = sortPreservedCategory(cat2); + } + + if (cat1 != cat2) { + return getCategoryIndex(cat1) - getCategoryIndex(cat2); + } + + // cat1 == cat2 implies preserved1 == preserved2 + + if (preserved1) { + return preserveRelativeOrder(bodyDeclaration1, bodyDeclaration2); + } + + if (sortByVisibility) { + int flags1 = JdtFlags.getVisibilityCode(bodyDeclaration1); + int flags2 = JdtFlags.getVisibilityCode(bodyDeclaration2); + int vis = getVisibilityIndex(flags1) - getVisibilityIndex(flags2); + if (vis != 0) { + return vis; + } + } + + switch (bodyDeclaration1.getNodeType()) { + case ASTNode.METHOD_DECLARATION: { + MethodDeclaration method1 = (MethodDeclaration) bodyDeclaration1; + MethodDeclaration method2 = (MethodDeclaration) bodyDeclaration2; + + if (sortByVisibility) { + int vis = getVisibilityIndex(method1.getModifiers()) - getVisibilityIndex(method2.getModifiers()); + if (vis != 0) { + return vis; + } + } + + String name1 = method1.getName().getIdentifier(); + String name2 = method2.getName().getIdentifier(); + + // method declarations (constructors) are sorted by name + int cmp = name1.compareTo(name2); + if (cmp != 0) { + return cmp; + } + + // if names equal, sort by parameter types + List parameters1 = method1.parameters(); + List parameters2 = method2.parameters(); + int length1 = parameters1.size(); + int length2 = parameters2.size(); + + int len = Math.min(length1, length2); + for (int i = 0; i < len; i++) { + SingleVariableDeclaration param1 = parameters1.get(i); + SingleVariableDeclaration param2 = parameters2.get(i); + cmp = buildSignature(param1.getType()).compareTo(buildSignature(param2.getType())); + if (cmp != 0) { + return cmp; + } + } + if (length1 != length2) { + return length1 - length2; + } + return preserveRelativeOrder(bodyDeclaration1, bodyDeclaration2); + } + case ASTNode.FIELD_DECLARATION: { + FieldDeclaration field1 = (FieldDeclaration) bodyDeclaration1; + FieldDeclaration field2 = (FieldDeclaration) bodyDeclaration2; + + String name1 = ((VariableDeclarationFragment) field1.fragments().get(0)).getName().getIdentifier(); + String name2 = ((VariableDeclarationFragment) field2.fragments().get(0)).getName().getIdentifier(); + + // field declarations are sorted by name + return compareNames(bodyDeclaration1, bodyDeclaration2, name1, name2); + } + case ASTNode.INITIALIZER: { + // preserve relative order + return preserveRelativeOrder(bodyDeclaration1, bodyDeclaration2); + } + case ASTNode.TYPE_DECLARATION: + case ASTNode.ENUM_DECLARATION: + case ASTNode.ANNOTATION_TYPE_DECLARATION: { + AbstractTypeDeclaration type1 = (AbstractTypeDeclaration) bodyDeclaration1; + AbstractTypeDeclaration type2 = (AbstractTypeDeclaration) bodyDeclaration2; + + String name1 = type1.getName().getIdentifier(); + String name2 = type2.getName().getIdentifier(); + + // typedeclarations are sorted by name + return compareNames(bodyDeclaration1, bodyDeclaration2, name1, name2); + } + case ASTNode.ENUM_CONSTANT_DECLARATION: { + EnumConstantDeclaration decl1 = (EnumConstantDeclaration) bodyDeclaration1; + EnumConstantDeclaration decl2 = (EnumConstantDeclaration) bodyDeclaration2; + + String name1 = decl1.getName().getIdentifier(); + String name2 = decl2.getName().getIdentifier(); + + // enum constants declarations are sorted by name + return compareNames(bodyDeclaration1, bodyDeclaration2, name1, name2); + } + case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION: { + AnnotationTypeMemberDeclaration decl1 = (AnnotationTypeMemberDeclaration) bodyDeclaration1; + AnnotationTypeMemberDeclaration decl2 = (AnnotationTypeMemberDeclaration) bodyDeclaration2; + + String name1 = decl1.getName().getIdentifier(); + String name2 = decl2.getName().getIdentifier(); + + // enum constants declarations are sorted by name + return compareNames(bodyDeclaration1, bodyDeclaration2, name1, name2); + } + } + return 0; + } + + private static int sortPreservedCategory(int category) { + switch (category) { + case STATIC_FIELDS_INDEX: + case STATIC_INIT_INDEX: + return STATIC_FIELDS_INDEX; + case FIELDS_INDEX: + case INIT_INDEX: + return FIELDS_INDEX; + default: + return category; + } + } + + private boolean isSortPreserved(BodyDeclaration bodyDeclaration) { + switch (bodyDeclaration.getNodeType()) { + case ASTNode.FIELD_DECLARATION: + case ASTNode.ENUM_CONSTANT_DECLARATION: + case ASTNode.INITIALIZER: + return true; + default: + return false; + } + } + + private int preserveRelativeOrder(BodyDeclaration bodyDeclaration1, BodyDeclaration bodyDeclaration2) { + int value1 = ((Integer) bodyDeclaration1.getProperty(CompilationUnitSorter.RELATIVE_ORDER)); + int value2 = ((Integer) bodyDeclaration2.getProperty(CompilationUnitSorter.RELATIVE_ORDER)); + return value1 - value2; + } + + private int compareNames(BodyDeclaration bodyDeclaration1, BodyDeclaration bodyDeclaration2, String name1, String name2) { + int cmp = name1.compareTo(name2); + if (cmp != 0) { + return cmp; + } + return preserveRelativeOrder(bodyDeclaration1, bodyDeclaration2); + } + + private String buildSignature(Type type) { + NaiveASTFlattener flattener = new NaiveASTFlattener(); + type.accept(flattener); + return flattener.getResult(); + } +} diff --git a/_ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImpl.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtFormatterStepImpl.java similarity index 57% rename from _ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImpl.java rename to lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtFormatterStepImpl.java index b3fb7acaf3..39a493e63b 100644 --- a/_ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/EclipseJdtFormatterStepImpl.java +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtFormatterStepImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,60 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.extra.eclipse.java; +package com.diffplug.spotless.extra.glue.jdt; import java.io.File; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; +import java.util.stream.Collectors; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.ToolFactory; import org.eclipse.jdt.core.formatter.CodeFormatter; import org.eclipse.jdt.internal.compiler.env.IModule; +import org.eclipse.jdt.internal.formatter.DefaultCodeFormatter; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipsePluginConfig; -import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseServiceConfig; - /** Formatter step which calls out to the Eclipse JDT formatter. */ public class EclipseJdtFormatterStepImpl { + /** Spotless demands for internal formatter chains Unix (LF) line endings. */ + public static final String LINE_DELIMITER = "\n"; private final CodeFormatter codeFormatter; + private final EclipseJdtSortMembers.SortProperties sortProperties; - public EclipseJdtFormatterStepImpl(Properties settings) throws Exception { - SpotlessEclipseFramework.setup(new FrameworkConfig()); - this.codeFormatter = ToolFactory.createCodeFormatter(settings, ToolFactory.M_FORMAT_EXISTING); - } - - private static class FrameworkConfig implements SpotlessEclipseConfig { - @Override - public void registerServices(SpotlessEclipseServiceConfig config) { - config.applyDefault(); - config.useSlf4J(EclipseJdtFormatterStepImpl.class.getPackage().getName()); - } - - @Override - public void activatePlugins(SpotlessEclipsePluginConfig config) { - config.applyDefault(); - config.add(new JavaCore()); - } - } - - /** @deprecated Use {@link #format(String, File)} instead. */ - @Deprecated - public String format(String raw) throws Exception { - return format(raw, new File("")); + public EclipseJdtFormatterStepImpl(Properties formatterSettings, Map sortProperties) { + Map options = formatterSettings.entrySet().stream().collect(Collectors.toMap( + e -> String.valueOf(e.getKey()), + e -> String.valueOf(e.getValue()), + (prev, next) -> next, + HashMap::new)); + this.codeFormatter = new DefaultCodeFormatter(options); + this.sortProperties = EclipseJdtSortMembers.SortProperties.from(sortProperties); } /** Formatting Java string, distinguishing module-info and compilation unit by file name */ public String format(String raw, File file) throws Exception { + raw = sort(raw); int kind = (file.getName().equals(IModule.MODULE_INFO_JAVA) ? CodeFormatter.K_MODULE_INFO : CodeFormatter.K_COMPILATION_UNIT) | CodeFormatter.F_INCLUDE_COMMENTS; - - TextEdit edit = codeFormatter.format(kind, raw, 0, raw.length(), 0, SpotlessEclipseFramework.LINE_DELIMITER); + TextEdit edit = codeFormatter.format(kind, raw, 0, raw.length(), 0, LINE_DELIMITER); if (edit == null) { throw new IllegalArgumentException("Invalid java syntax for formatting."); } else { @@ -76,4 +61,8 @@ public String format(String raw, File file) throws Exception { } } + /** Sort members in Java string */ + public String sort(String raw) { + return EclipseJdtSortMembers.sortMember(raw, sortProperties); + } } diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtSortMembers.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtSortMembers.java new file mode 100644 index 0000000000..05498b1caf --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtSortMembers.java @@ -0,0 +1,247 @@ +/* + * Copyright 2024 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.glue.jdt; + +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.IBufferChangedListener; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IOpenable; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.internal.core.SortElementsOperation; + +public class EclipseJdtSortMembers { + + private static final Pattern PATTERN_DO_NOT_SORT_FIELDS = Pattern.compile("@SortMembers:doNotSortFields\\s*=\\s*(false|true)"); + private static final Pattern PATTERN_ENABLED = Pattern.compile("@SortMembers:enabled\\s*=\\s*(false|true)"); + private static final Pattern PATTERN_SORT_BY_VISIBILITY = Pattern.compile("@SortMembers:sortByVisibility\\s*=\\s*(false|true)"); + + static SortProperties localProperties(SortProperties globalProperties, String code) { + Optional localDoNotSortFields = testOverwriteProperty(PATTERN_DO_NOT_SORT_FIELDS, code); + Optional localEnabled = testOverwriteProperty(PATTERN_ENABLED, code); + Optional localSortByVisibility = testOverwriteProperty(PATTERN_SORT_BY_VISIBILITY, code); + if (localDoNotSortFields.isEmpty() && localEnabled.isEmpty() && localSortByVisibility.isEmpty()) { + return globalProperties; + } + boolean doNotSortFields = localDoNotSortFields.orElse(globalProperties.doNotSortFields); + boolean enabled = localEnabled.orElse(globalProperties.enabled); + boolean sortByVisibility = localSortByVisibility.orElse(globalProperties.sortByVisibility); + return new SortProperties( + enabled, + globalProperties.membersOrder, + doNotSortFields, + sortByVisibility, + globalProperties.visibilityOrder); + } + + static String sortMember(String code, SortProperties globalProperties) { + SortProperties localProperties = localProperties(globalProperties, code); + if (!localProperties.enabled) { + return code; + } + + try { + CompilationUnit compilationUnit = new CompilationUnit(code); + DefaultJavaElementComparator comparator = DefaultJavaElementComparator.of( + localProperties.doNotSortFields, + localProperties.membersOrder, + localProperties.sortByVisibility, + localProperties.visibilityOrder); + new Sorter(AST.getJLSLatest(), compilationUnit, null, comparator).sort(); + String content = compilationUnit.getBuffer().getContents(); + if (content != null) { + code = content; + } + } catch (CoreException e) { + throw new RuntimeException(e); + } + return code; + } + + static Optional testOverwriteProperty(Pattern pattern, String code) { + Matcher matcher = pattern.matcher(code); + if (matcher.find()) { + String flag = matcher.group(1); + return Optional.of(Boolean.valueOf(flag)); + } + return Optional.empty(); + } + + private static class Buffer implements IBuffer { + + private String contents; + + Buffer(String contents) { + this.contents = contents; + } + + public void addBufferChangedListener(IBufferChangedListener listener) {} + + public void append(char[] text) {} + + public void append(String text) {} + + public void close() {} + + public char getChar(int position) { + return '\u0000'; + } + + public char[] getCharacters() { + return contents.toCharArray(); + } + + public String getContents() { + return contents; + } + + public int getLength() { + return 0; + } + + public IOpenable getOwner() { + return null; + } + + public String getText(int offset, int length) { + return null; + } + + public IResource getUnderlyingResource() { + return null; + } + + public boolean hasUnsavedChanges() { + return false; + } + + public boolean isClosed() { + return false; + } + + public boolean isReadOnly() { + return true; + } + + public void removeBufferChangedListener(IBufferChangedListener listener) {} + + public void replace(int position, int length, char[] text) {} + + public void replace(int position, int length, String text) {} + + public void save(IProgressMonitor progress, boolean force) {} + + public void setContents(char[] contents) {} + + public void setContents(String contents) { + this.contents = contents; + } + } + + @SuppressFBWarnings(value = "EQ_DOESNT_OVERRIDE_EQUALS", justification = "the equals method shouldn't be called in the sort members use case") + private static class CompilationUnit extends org.eclipse.jdt.internal.core.CompilationUnit { + private final Buffer buffer; + + CompilationUnit(String code) { + super(null, null, null); + buffer = new Buffer(code); + } + + @Override + public IBuffer getBuffer() { + return buffer; + } + + @Override + public JavaProject getJavaProject() { + return JavaProject.INSTANCE; + } + + @Override + public Map getOptions(boolean inheritJavaCoreOptions) { + return Map.of(); + } + + @Override + public ICompilationUnit getPrimary() { + return this; + } + } + + private static class JavaProject extends org.eclipse.jdt.internal.core.JavaProject { + static final JavaProject INSTANCE = new JavaProject(); + + JavaProject() { + super(null, null); + } + + @Override + public Map getOptions(boolean inheritJavaCoreOptions) { + return Map.of(); + } + } + + private static class Sorter extends SortElementsOperation { + + Sorter(int level, CompilationUnit compilationUnit, int[] positions, Comparator comparator) { + super(level, new IJavaElement[]{compilationUnit}, positions, comparator); + } + + void sort() throws JavaModelException { + executeOperation(); + } + } + + static class SortProperties { + final boolean doNotSortFields; + final boolean enabled; + final String membersOrder; + final boolean sortByVisibility; + final String visibilityOrder; + + SortProperties( + boolean enabled, + String membersOrder, + boolean doNotSortFields, + boolean sortByVisibility, + String visibilityOrder) { + this.enabled = enabled; + this.membersOrder = membersOrder; + this.doNotSortFields = doNotSortFields; + this.sortByVisibility = sortByVisibility; + this.visibilityOrder = visibilityOrder; + } + + static SortProperties from(Map properties) { + boolean enabled = Boolean.parseBoolean(properties.getOrDefault("sp_cleanup.sort_members", "false")); + String membersOrder = properties.getOrDefault("outlinesortoption", ""); + boolean doNotSortFields = !Boolean.parseBoolean(properties.getOrDefault("sp_cleanup.sort_members_all", "false")); + boolean sortByVisibility = Boolean.parseBoolean(properties.getOrDefault("org.eclipse.jdt.ui.enable.visibility.order", "false")); + String visibilityOrder = properties.getOrDefault("org.eclipse.jdt.ui.visibility.order", ""); + return new SortProperties(enabled, membersOrder, doNotSortFields, sortByVisibility, visibilityOrder); + } + } +} diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JdtFlags.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JdtFlags.java new file mode 100644 index 0000000000..819168013f --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JdtFlags.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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.glue.jdt; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.EnumConstantDeclaration; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.TypeDeclaration; + +/** + * This class is derived and adapted code from the Eclipse JDT project (Derivative Works according to EPL 2.0 license). + */ +class JdtFlags { + + static final int VISIBILITY_CODE_INVALID = -1; + + static boolean isStatic(BodyDeclaration bodyDeclaration) { + if (isNestedInterfaceOrAnnotation(bodyDeclaration)) + return true; + int nodeType = bodyDeclaration.getNodeType(); + if (nodeType != ASTNode.METHOD_DECLARATION + && nodeType != ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION + && isInterfaceOrAnnotationMember(bodyDeclaration)) + return true; + if (bodyDeclaration instanceof EnumConstantDeclaration) + return true; + if (bodyDeclaration instanceof EnumDeclaration && bodyDeclaration.getParent() instanceof AbstractTypeDeclaration) + return true; + return Modifier.isStatic(bodyDeclaration.getModifiers()); + } + + private static boolean isPackageVisible(BodyDeclaration bodyDeclaration) { + return (!isPrivate(bodyDeclaration) && !isProtected(bodyDeclaration) && !isPublic(bodyDeclaration)); + } + + private static boolean isPrivate(BodyDeclaration bodyDeclaration) { + return Modifier.isPrivate(bodyDeclaration.getModifiers()); + } + + private static boolean isProtected(BodyDeclaration bodyDeclaration) { + return Modifier.isProtected(bodyDeclaration.getModifiers()); + } + + private static boolean isPublic(BodyDeclaration bodyDeclaration) { + if (isInterfaceOrAnnotationMember(bodyDeclaration)) + return true; + return Modifier.isPublic(bodyDeclaration.getModifiers()); + } + + private static boolean isInterfaceOrAnnotationMember(BodyDeclaration bodyDeclaration) { + return isInterfaceOrAnnotation(bodyDeclaration.getParent()); + } + + private static boolean isInterfaceOrAnnotation(ASTNode node) { + boolean isInterface = (node instanceof TypeDeclaration) && ((TypeDeclaration) node).isInterface(); + boolean isAnnotation = node instanceof AnnotationTypeDeclaration; + return isInterface || isAnnotation; + } + + private static boolean isNestedInterfaceOrAnnotation(BodyDeclaration bodyDeclaration) { + return bodyDeclaration.getParent() instanceof AbstractTypeDeclaration && isInterfaceOrAnnotation(bodyDeclaration); + } + + static int getVisibilityCode(BodyDeclaration bodyDeclaration) { + if (isPublic(bodyDeclaration)) + return Modifier.PUBLIC; + else if (isProtected(bodyDeclaration)) + return Modifier.PROTECTED; + else if (isPackageVisible(bodyDeclaration)) + return Modifier.NONE; + else if (isPrivate(bodyDeclaration)) + return Modifier.PRIVATE; + Assert.isTrue(false); + return VISIBILITY_CODE_INVALID; + } +} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/package-info.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/SuppressFBWarnings.java similarity index 65% rename from _ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/package-info.java rename to lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/SuppressFBWarnings.java index 6cf6f0bfff..bfcad72a23 100644 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/package-info.java +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/SuppressFBWarnings.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Eclipse WTP based Spotless formatters */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.wtp; +package com.diffplug.spotless.extra.glue.jdt; -import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.CLASS) +@interface SuppressFBWarnings { + String[] value() default {}; + + String justification() default ""; +} 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 4f48a5ed74..0dff3f6898 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Properties; import com.diffplug.common.base.Errors; @@ -34,7 +33,7 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.SerializedFunction; /** * Generic Eclipse based formatter step {@link State} builder. @@ -42,7 +41,7 @@ public class EclipseBasedStepBuilder { private final String formatterName; private final String formatterStepExt; - private final ThrowingEx.Function stateToFormatter; + private final SerializedFunction stateToFormatter; private final Provisioner jarProvisioner; private String formatterVersion; @@ -64,12 +63,12 @@ public class EclipseBasedStepBuilder { private Iterable settingsFiles = new ArrayList<>(); /** Initialize valid default configuration, taking latest version */ - public EclipseBasedStepBuilder(String formatterName, Provisioner jarProvisioner, ThrowingEx.Function stateToFormatter) { + public EclipseBasedStepBuilder(String formatterName, Provisioner jarProvisioner, SerializedFunction stateToFormatter) { this(formatterName, "", jarProvisioner, stateToFormatter); } /** Initialize valid default configuration, taking latest version */ - public EclipseBasedStepBuilder(String formatterName, String formatterStepExt, Provisioner jarProvisioner, ThrowingEx.Function stateToFormatter) { + public EclipseBasedStepBuilder(String formatterName, String formatterStepExt, Provisioner jarProvisioner, SerializedFunction stateToFormatter) { this.formatterName = Objects.requireNonNull(formatterName, "formatterName"); this.formatterStepExt = Objects.requireNonNull(formatterStepExt, "formatterStepExt"); this.jarProvisioner = Objects.requireNonNull(jarProvisioner, "jarProvisioner"); @@ -79,7 +78,11 @@ public EclipseBasedStepBuilder(String formatterName, String formatterStepExt, Pr /** Returns the FormatterStep (whose state will be calculated lazily). */ public FormatterStep build() { - return FormatterStep.createLazy(formatterName + formatterStepExt, this::get, stateToFormatter); + var roundtrippableState = new EclipseStep(formatterVersion, formatterStepExt, FileSignature.promise(settingsFiles), JarState.promise(() -> { + return JarState.withoutTransitives(dependencies, jarProvisioner); + })); + return FormatterStep.create(formatterName + formatterStepExt, roundtrippableState, + EclipseStep::state, stateToFormatter); } /** Set dependencies for the corresponding Eclipse version */ @@ -123,21 +126,23 @@ public void setPreferences(Iterable settingsFiles) { this.settingsFiles = settingsFiles; } - /** Creates the state of the configuration. */ - EclipseBasedStepBuilder.State get() throws IOException { - /* - * The current use case is tailored for Gradle. - * Gradle calls this method only once per execution - * and compares the State with the one of a previous run - * for incremental building. - * Hence a lazy construction is not required. - */ - return new State( - formatterVersion, - formatterStepExt, - jarProvisioner, - dependencies, - settingsFiles); + static class EclipseStep implements Serializable { + private static final long serialVersionUID = 1; + private final String semanticVersion; + private final String formatterStepExt; + private final FileSignature.Promised settingsPromise; + private final JarState.Promised jarPromise; + + EclipseStep(String semanticVersion, String formatterStepExt, FileSignature.Promised settingsPromise, JarState.Promised jarPromise) { + this.semanticVersion = semanticVersion; + this.formatterStepExt = formatterStepExt; + this.settingsPromise = settingsPromise; + this.jarPromise = jarPromise; + } + + private State state() { + return new State(semanticVersion, formatterStepExt, jarPromise.get(), settingsPromise.get()); + } } /** @@ -156,9 +161,9 @@ public static class State implements Serializable { private final FileSignature settingsFiles; /** State constructor expects that all passed items are not modified afterwards */ - protected State(String formatterVersion, String formatterStepExt, Provisioner jarProvisioner, List dependencies, Iterable settingsFiles) throws IOException { - this.jarState = JarState.withoutTransitives(dependencies, jarProvisioner); - this.settingsFiles = FileSignature.signAsList(settingsFiles); + protected State(String formatterVersion, String formatterStepExt, JarState jarState, FileSignature settingsFiles) { + this.jarState = jarState; + this.settingsFiles = settingsFiles; this.formatterStepExt = formatterStepExt; semanticVersion = convertEclipseVersion(formatterVersion); } @@ -187,12 +192,6 @@ public Properties getPreferences() { return preferences.getProperties(); } - /** Returns first coordinate from sorted set that starts with a given prefix.*/ - public Optional getMavenCoordinate(String prefix) { - return jarState.getMavenCoordinates().stream() - .filter(coordinate -> coordinate.startsWith(prefix)).findFirst(); - } - /** * Load class based on the given configuration of JAR provider and Maven coordinates. * Different class loader instances are provided in the following scenarios: 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 new file mode 100644 index 0000000000..fa68bc5391 --- /dev/null +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/EquoBasedStepBuilder.java @@ -0,0 +1,235 @@ +/* + * Copyright 2016-2025 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 java.util.stream.Collectors.toMap; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; + +import javax.annotation.Nullable; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterProperties; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.SerializedFunction; + +import dev.equo.solstice.NestedJars; +import dev.equo.solstice.p2.CacheLocations; +import dev.equo.solstice.p2.P2ClientCache; +import dev.equo.solstice.p2.P2Model; +import dev.equo.solstice.p2.P2QueryCache; +import dev.equo.solstice.p2.P2QueryResult; + +/** + * Generic Eclipse based formatter step {@link State} builder. + */ +public abstract class EquoBasedStepBuilder { + private final String formatterName; + private final Provisioner mavenProvisioner; + private final SerializedFunction stateToFormatter; + private final ImmutableMap.Builder stepProperties; + private String formatterVersion; + private Iterable settingsFiles = new ArrayList<>(); + private List settingProperties = new ArrayList<>(); + private Map p2Mirrors = Map.of(); + private File cacheDirectory; + + /** Initialize valid default configuration, taking latest version */ + public EquoBasedStepBuilder( + String formatterName, + Provisioner mavenProvisioner, + @Nullable String defaultVersion, + SerializedFunction stateToFormatter, + ImmutableMap.Builder stepProperties) { + + this.formatterName = formatterName; + this.mavenProvisioner = mavenProvisioner; + this.formatterVersion = defaultVersion; + this.stateToFormatter = stateToFormatter; + this.stepProperties = stepProperties; + } + + public void setVersion(String version) { + formatterVersion = version; + } + + public void setPreferences(Iterable settingsFiles) { + this.settingsFiles = settingsFiles; + } + + public void setPropertyPreferences(List propertyPreferences) { + this.settingProperties = propertyPreferences; + } + + public void setP2Mirrors(Map p2Mirrors) { + this.p2Mirrors = Map.copyOf(p2Mirrors); + } + + public void setP2Mirrors(Collection p2Mirrors) { + this.p2Mirrors = p2Mirrors.stream().collect(toMap(P2Mirror::getPrefix, P2Mirror::getUrl)); + } + + public void setCacheDirectory(File cacheDirectory) { + this.cacheDirectory = cacheDirectory; + } + + protected abstract P2Model model(String version); + + protected void addPlatformRepo(P2Model model, String version) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected 4.x"); + } + int minorVersion = Integer.parseInt(version.substring("4.".length())); + + model.addP2Repo("https://download.eclipse.org/eclipse/updates/" + version + "/"); + model.getInstall().addAll(List.of( + "org.apache.felix.scr", + "org.eclipse.equinox.event")); + if (minorVersion >= 25) { + model.getInstall().addAll(List.of( + "org.osgi.service.cm", + "org.osgi.service.metatype")); + } + } + + /** Returns the FormatterStep (whose state will be calculated lazily). */ + public FormatterStep build() { + var roundtrippableState = new EquoStep(formatterVersion, settingProperties, FileSignature.promise(settingsFiles), JarState.promise(() -> { + P2QueryResult query; + try { + if (null != cacheDirectory) { + CacheLocations.override_p2data = cacheDirectory.toPath().resolve("dev/equo/p2-data").toFile(); + } + query = createModelWithMirrors().query(P2ClientCache.PREFER_OFFLINE, P2QueryCache.ALLOW); + } catch (Exception x) { + throw new IOException("Failed to load " + formatterName + ": " + x, x); + } + 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()); + } + return JarState.preserveOrder(classpath); + }), stepProperties.build()); + return FormatterStep.create(formatterName, roundtrippableState, EquoStep::state, stateToFormatter); + } + + private P2Model createModelWithMirrors() { + P2Model model = model(formatterVersion); + if (p2Mirrors.isEmpty()) { + return model; + } + + ArrayList p2Repos = new ArrayList<>(model.getP2repo()); + p2Repos.replaceAll(url -> { + for (Map.Entry mirror : p2Mirrors.entrySet()) { + String prefix = mirror.getKey(); + if (url.startsWith(prefix)) { + return mirror.getValue() + url.substring(prefix.length()); + } + } + + throw new IllegalStateException("no mirror configured for P2 repository: " + url); + }); + + model.getP2repo().clear(); + model.getP2repo().addAll(p2Repos); + return model; + } + + static class EquoStep implements Serializable { + private static final long serialVersionUID = 1; + private final String semanticVersion; + private final FileSignature.Promised settingsPromise; + private final JarState.Promised jarPromise; + private final ImmutableMap stepProperties; + private List settingProperties; + + EquoStep( + String semanticVersion, + List settingProperties, + FileSignature.Promised settingsPromise, + JarState.Promised jarPromise, + ImmutableMap stepProperties) { + + this.semanticVersion = semanticVersion; + this.settingProperties = Optional.ofNullable(settingProperties).orElse(new ArrayList<>()); + this.settingsPromise = settingsPromise; + this.jarPromise = jarPromise; + this.stepProperties = stepProperties; + } + + private State state() { + return new State(semanticVersion, jarPromise.get(), settingProperties, settingsPromise.get(), stepProperties); + } + } + + /** + * State of Eclipse configuration items, providing functionality to derived information + * based on the state. + */ + public static class State implements Serializable { + private static final long serialVersionUID = 1; + final String semanticVersion; + final JarState jarState; + final FileSignature settingsFiles; + final ImmutableMap stepProperties; + private List settingProperties; + + public State(String semanticVersion, JarState jarState, List settingProperties, FileSignature settingsFiles, ImmutableMap stepProperties) { + this.semanticVersion = semanticVersion; + this.jarState = jarState; + this.settingProperties = Optional.ofNullable(settingProperties).orElse(new ArrayList<>()); + this.settingsFiles = settingsFiles; + this.stepProperties = stepProperties; + } + + public JarState getJarState() { + return jarState; + } + + public String getSemanticVersion() { + return semanticVersion; + } + + public Properties getPreferences() { + FormatterProperties fromFiles = FormatterProperties.from(settingsFiles.files()); + FormatterProperties fromPropertiesContent = FormatterProperties.fromPropertiesContent(settingProperties); + return FormatterProperties.merge(fromFiles.getProperties(), fromPropertiesContent.getProperties()).getProperties(); + } + + public ImmutableMap getStepProperties() { + return stepProperties; + } + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java index 70fca79027..0e058566f2 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package com.diffplug.spotless.extra; -import static com.diffplug.spotless.extra.LibExtraPreconditions.requireElementsNonNull; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -38,11 +36,14 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; import org.eclipse.jgit.lib.CoreConfig.EOL; import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.SystemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharSequenceNodeFactory; @@ -51,19 +52,58 @@ import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.extra.GitWorkarounds.RepositorySpecificResolver; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Uses .gitattributes to determine - * the appropriate line ending. Falls back to the {@code core.eol} property in the + * the appropriate line ending. Falls back to the {@code core.eol} and {@code core.autocrlf} properties in the * git config if there are no applicable git attributes, then finally falls * back to the platform native. */ public final class GitAttributesLineEndings { + private static final Logger LOGGER = LoggerFactory.getLogger(GitAttributesLineEndings.class); + // prevent direct instantiation private GitAttributesLineEndings() {} + /** + * Creates a line-endings policy which matches {@link #create(File, Supplier)}, + * which is much faster at the cost that every file under the policy + * is assumed to have the same line endings as the first file. + */ + public static LineEnding.Policy createFastAllSame(File projectDir, Supplier> toFormat) { + return new LazyAllTheSame(projectDir, toFormat); + } + + static class LazyAllTheSame extends LazyForwardingEquality implements LineEnding.Policy { + private static final long serialVersionUID = 727912266173243664L; + transient File projectDir; + transient Supplier> toFormat; + + public LazyAllTheSame(File projectDir, Supplier> toFormat) { + this.projectDir = projectDir; + this.toFormat = toFormat; + } + + @Override + protected String calculateState() throws Exception { + var files = toFormat.get().iterator(); + if (files.hasNext()) { + Runtime runtime = new RuntimeInit(projectDir).atRuntime(); + return runtime.getEndingFor(files.next()); + } else { + return LineEnding.UNIX.str(); + } + } + + @Override + public String getEndingFor(File file) { + return state(); + } + } + /** * Creates a line-endings policy whose serialized state is relativized against projectDir, * at the cost of eagerly evaluating the line-ending state of every target file when the @@ -76,8 +116,8 @@ public static LineEnding.Policy create(File projectDir, Supplier> static class RelocatablePolicy extends LazyForwardingEquality implements LineEnding.Policy { private static final long serialVersionUID = 5868522122123693015L; - final transient File projectDir; - final transient Supplier> toFormat; + transient File projectDir; + transient Supplier> toFormat; RelocatablePolicy(File projectDir, Supplier> toFormat) { this.projectDir = Objects.requireNonNull(projectDir, "projectDir"); @@ -86,8 +126,13 @@ static class RelocatablePolicy extends LazyForwardingEquality imp @Override protected CachedEndings calculateState() throws Exception { - Runtime runtime = new RuntimeInit(projectDir, toFormat.get()).atRuntime(); - return new CachedEndings(projectDir, runtime, toFormat.get()); + Runtime runtime = new RuntimeInit(projectDir).atRuntime(); + // LazyForwardingEquality guarantees that this will only be called once, and keeping toFormat + // causes a memory leak, see https://github.com/diffplug/spotless/issues/1194 + CachedEndings state = new CachedEndings(projectDir, runtime, toFormat.get()); + projectDir = null; + toFormat = null; + return state; } @Override @@ -131,8 +176,11 @@ public String endingFor(File file) { } static class RuntimeInit { - /** /etc/gitconfig (system-global), ~/.gitconfig, project/.git/config (each might-not exist). */ - final FileBasedConfig systemConfig, userConfig, repoConfig; + /** /etc/gitconfig (system-global), ~/.gitconfig (each might-not exist). */ + final FileBasedConfig systemConfig, userConfig; + + /** Repository specific config, can be $GIT_COMMON_DIR/config, project/.git/config or .git/worktrees//config.worktree if enabled by extension */ + final Config repoConfig; /** Global .gitattributes file pointed at by systemConfig or userConfig, and the file in the repo. */ final @Nullable File globalAttributesFile, repoAttributesFile; @@ -141,19 +189,19 @@ static class RuntimeInit { final @Nullable File workTree; @SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON") - RuntimeInit(File projectDir, Iterable toFormat) throws IOException { - requireElementsNonNull(toFormat); + RuntimeInit(File projectDir) { ///////////////////////////////// // USER AND SYSTEM-WIDE VALUES // ///////////////////////////////// + FS.DETECTED.setGitSystemConfig(new File("no-global-git-config-for-spotless")); // this fixes a problem + // that was only occurring on Java 11. If we remove support for Java 11, we could probably remove it. systemConfig = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED); Errors.log().run(systemConfig::load); userConfig = SystemReader.getInstance().openUserConfig(systemConfig, FS.DETECTED); Errors.log().run(userConfig::load); - // copy-pasted from org.eclipse.jgit.lib.CoreConfig - String globalAttributesPath = userConfig.getString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_ATTRIBUTESFILE); // copy-pasted from org.eclipse.jgit.internal.storage.file.GlobalAttributesNode + String globalAttributesPath = userConfig.get(CoreConfig.KEY).getAttributesFile(); if (globalAttributesPath != null) { FS fs = FS.detect(); if (globalAttributesPath.startsWith("~/")) { //$NON-NLS-1$ @@ -168,29 +216,16 @@ static class RuntimeInit { ////////////////////////// // REPO-SPECIFIC VALUES // ////////////////////////// - FileRepositoryBuilder builder = GitWorkarounds.fileRepositoryBuilderForProject(projectDir); - if (builder.getGitDir() != null) { - workTree = builder.getWorkTree(); - repoConfig = new FileBasedConfig(userConfig, new File(builder.getGitDir(), Constants.CONFIG), FS.DETECTED); - repoAttributesFile = new File(builder.getGitDir(), Constants.INFO_ATTRIBUTES); + RepositorySpecificResolver repositoryResolver = GitWorkarounds.fileRepositoryResolverForProject(projectDir, userConfig); + if (repositoryResolver.getGitDir() != null) { + workTree = repositoryResolver.getWorkTree(); + repoConfig = repositoryResolver.getRepositoryConfig(); + repoAttributesFile = repositoryResolver.resolveWithCommonDir(Constants.INFO_ATTRIBUTES); } else { workTree = null; - // null would make repoConfig.getFile() bomb below - repoConfig = new FileBasedConfig(userConfig, null, FS.DETECTED) { - @Override - public void load() { - // empty, do not load - } - - @Override - public boolean isOutdated() { - // regular class would bomb here - return false; - } - }; + repoConfig = new Config(); repoAttributesFile = null; } - Errors.log().run(repoConfig::load); } private Runtime atRuntime() { @@ -213,7 +248,7 @@ static class Runtime { /** * Default line ending, determined in this order (paths are a teensy different platform to platform). - * + *

* - .git/config (per-repo) * - ~/.gitconfig (per-user) * - /etc/gitconfig (system-wide) @@ -224,7 +259,7 @@ static class Runtime { private Runtime(List infoRules, @Nullable File workTree, Config config, List globalRules) { this.infoRules = Objects.requireNonNull(infoRules); this.workTree = workTree; - this.defaultEnding = fromEol(config.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EOL, EOL.NATIVE)).str(); + this.defaultEnding = findDefaultLineEnding(config).str(); this.globalRules = Objects.requireNonNull(globalRules); } @@ -268,11 +303,33 @@ private static String convertEolToLineEnding(String eol, File file) { case "crlf": return LineEnding.WINDOWS.str(); default: - System.err.println(".gitattributes file has unspecified eol value: " + eol + " for " + file + ", defaulting to platform native"); + LOGGER.warn(".gitattributes file has unspecified eol value: {} for {}, defaulting to platform native", eol, file); return LineEnding.PLATFORM_NATIVE.str(); } } + private LineEnding findDefaultLineEnding(Config config) { + // handle core.autocrlf, whose values "true" and "input" override core.eol + AutoCRLF autoCRLF = config.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOCRLF, AutoCRLF.FALSE); + if (autoCRLF == AutoCRLF.TRUE) { + // autocrlf=true converts CRLF->LF during commit + // and converts LF->CRLF during checkout + // so CRLF is the default line ending + return LineEnding.WINDOWS; + } else if (autoCRLF == AutoCRLF.INPUT) { + // autocrlf=input converts CRLF->LF during commit + // and does no conversion during checkout + // mostly used on Unix, so LF is the default encoding + return LineEnding.UNIX; + } else if (autoCRLF == AutoCRLF.FALSE) { + // handle core.eol + EOL eol = config.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EOL, EOL.NATIVE); + return fromEol(eol); + } else { + throw new IllegalStateException("Unexpected value for autoCRLF " + autoCRLF); + } + } + /** Creates a LineEnding from an EOL. */ private static LineEnding fromEol(EOL eol) { // @formatter:off @@ -326,8 +383,7 @@ private static List parseRules(@Nullable File file) { return parsed.getRules(); } catch (IOException e) { // no need to crash the whole plugin - System.err.println("Problem parsing " + file.getAbsolutePath()); - e.printStackTrace(); + LOGGER.warn("Problem parsing {}", file.getAbsolutePath(), e); } } return Collections.emptyList(); diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java index 6b8d5a9f41..037d4d847b 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 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,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Optional; import javax.annotation.Nullable; @@ -137,7 +136,7 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) { private final static int INDEX = 1; private final static int WORKDIR = 2; - Map gitRoots = new HashMap<>(); + Map gitRoots = new HashMap<>(); Table rootTreeShaCache = HashBasedTable.create(); Map subtreeShaCache = new HashMap<>(); @@ -147,25 +146,14 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) { * We cache the Repository for every Project in {@code gitRoots}, and use dynamic programming to populate it. */ protected Repository repositoryFor(Project project) throws IOException { - Repository repo = gitRoots.get(project); + File projectGitDir = GitWorkarounds.getDotGitDir(getDir(project)); + if (projectGitDir == null || !RepositoryCache.FileKey.isGitRepository(projectGitDir, FS.DETECTED)) { + throw new IllegalArgumentException("Cannot find git repository in any parent directory"); + } + Repository repo = gitRoots.get(projectGitDir); if (repo == null) { - if (isGitRoot(getDir(project))) { - repo = createRepo(getDir(project)); - } else { - Project parentProj = getParent(project); - if (parentProj == null) { - repo = traverseParentsUntil(getDir(project).getParentFile(), null); - if (repo == null) { - throw new IllegalArgumentException("Cannot find git repository in any parent directory"); - } - } else { - repo = traverseParentsUntil(getDir(project).getParentFile(), getDir(parentProj)); - if (repo == null) { - repo = repositoryFor(parentProj); - } - } - } - gitRoots.put(project, repo); + repo = FileRepositoryBuilder.create(projectGitDir); + gitRoots.put(projectGitDir, repo); } return repo; } @@ -174,26 +162,6 @@ protected Repository repositoryFor(Project project) throws IOException { protected abstract @Nullable Project getParent(Project project); - private static @Nullable Repository traverseParentsUntil(File startWith, @Nullable File file) throws IOException { - while (startWith != null && !Objects.equals(startWith, file)) { - if (isGitRoot(startWith)) { - return createRepo(startWith); - } else { - startWith = startWith.getParentFile(); - } - } - return null; - } - - private static boolean isGitRoot(File dir) { - File dotGit = GitWorkarounds.getDotGitDir(dir); - return dotGit != null && RepositoryCache.FileKey.isGitRepository(dotGit, FS.DETECTED); - } - - static Repository createRepo(File dir) throws IOException { - return FileRepositoryBuilder.create(GitWorkarounds.getDotGitDir(dir)); - } - /** * Fast way to return treeSha of the given ref against the git repository which stores the given project. * Because of parallel project evaluation, there may be races here, so we synchronize on ourselves. However, this method diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java index ee13fefc81..94065f0059 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,22 +17,32 @@ import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import javax.annotation.Nullable; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.SystemReader; + +import com.diffplug.common.base.Errors; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Utility methods for Git workarounds. */ -public class GitWorkarounds { +public final class GitWorkarounds { private GitWorkarounds() {} /** * Finds the .git directory for the given project directory. - * + *

* Ordinarily one would just use JGit for this, but it doesn't support worktrees properly. * So this applies an additional workaround for that. * @@ -40,46 +50,183 @@ private GitWorkarounds() {} * @return the path to the .git directory. */ static @Nullable File getDotGitDir(File projectDir) { - return fileRepositoryBuilderForProject(projectDir).getGitDir(); + return fileRepositoryResolverForProject(projectDir).getGitDir(); } /** - * Creates a {@link FileRepositoryBuilder} for the given project directory. + * Creates a {@link RepositorySpecificResolver} for the given project directory. + *

+ * This applies a workaround for JGit not supporting worktrees properly. * + * @param projectDir the project directory. + * @return the builder. + */ + static RepositorySpecificResolver fileRepositoryResolverForProject(File projectDir) { + return fileRepositoryResolverForProject(projectDir, null); + } + + /** + * Creates a {@link RepositorySpecificResolver} for the given project directory. + *

* This applies a workaround for JGit not supporting worktrees properly. * * @param projectDir the project directory. + * @param baseConfig the user and system level git config. * @return the builder. */ - static FileRepositoryBuilder fileRepositoryBuilderForProject(File projectDir) { - FileRepositoryBuilder builder = new FileRepositoryBuilder(); - builder.findGitDir(projectDir); - File gitDir = builder.getGitDir(); - if (gitDir != null) { - builder.setGitDir(resolveRealGitDirIfWorktreeDir(gitDir)); + static RepositorySpecificResolver fileRepositoryResolverForProject(File projectDir, @Nullable Config baseConfig) { + RepositorySpecificResolver repositoryResolver = new RepositorySpecificResolver(baseConfig); + repositoryResolver.findGitDir(projectDir); + repositoryResolver.readEnvironment(); + if (repositoryResolver.getGitDir() != null || repositoryResolver.getWorkTree() != null) { + Errors.rethrow().get(repositoryResolver::setup); } - return builder; + return repositoryResolver; } /** - * If the dir is a worktree directory (typically .git/worktrees/something) then - * returns the actual .git directory. - * - * @param dir the directory which may be a worktree directory or may be a .git directory. - * @return the .git directory. + * Piggyback on the {@link FileRepositoryBuilder} mechanics for finding the git directory. + *

+ * Here we take into account that git repositories can share a common directory. This directory + * will contain ./config ./objects/, ./info/, and ./refs/. */ - private static File resolveRealGitDirIfWorktreeDir(File dir) { - File pointerFile = new File(dir, "gitdir"); - if (pointerFile.isFile()) { - try { - String content = new String(Files.readAllBytes(pointerFile.toPath()), StandardCharsets.UTF_8).trim(); - return new File(content); - } catch (IOException e) { - System.err.println("failed to parse git meta: " + e.getMessage()); - return dir; + static class RepositorySpecificResolver extends FileRepositoryBuilder { + /** + * The common directory file is used to define $GIT_COMMON_DIR if environment variable is not set. + * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/gitrepository-layout.txt#L259 + */ + private static final String COMMON_DIR = "commondir"; + private static final String GIT_COMMON_DIR_ENV_KEY = "GIT_COMMON_DIR"; + + /** + * Using an extension it is possible to have per-worktree config. + * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/git-worktree.txt#L366 + */ + private static final String EXTENSIONS_WORKTREE_CONFIG = "worktreeConfig"; + private static final String EXTENSIONS_WORKTREE_CONFIG_FILENAME = "config.worktree"; + + private File commonDirectory; + + private Config baseConfig; + + public RepositorySpecificResolver() { + this(null); + } + + public RepositorySpecificResolver(@Nullable Config baseConfig) { + this.baseConfig = baseConfig; + } + + /** @return the repository specific configuration. */ + Config getRepositoryConfig() { + return Errors.rethrow().get(this::getConfig); + } + + /** + * @return the repository's configuration. + * @throws IOException on errors accessing the configuration file. + * @throws IllegalArgumentException on malformed configuration. + */ + @Override + protected Config loadConfig() throws IOException { + if (getGitDir() != null) { + File path = resolveWithCommonDir(Constants.CONFIG); + FileBasedConfig cfg = null; + if (this.baseConfig == null) { + cfg = new FileBasedConfig(path, safeFS()); + } else { + cfg = new FileBasedConfig(baseConfig, path, safeFS()); + } + try { + cfg.load(); + + // Check for per-worktree config, it should be parsed after the common config + if (cfg.getBoolean(ConfigConstants.CONFIG_EXTENSIONS_SECTION, EXTENSIONS_WORKTREE_CONFIG, false)) { + File worktreeSpecificConfig = safeFS().resolve(getGitDir(), EXTENSIONS_WORKTREE_CONFIG_FILENAME); + if (safeFS().exists(worktreeSpecificConfig) && safeFS().isFile(worktreeSpecificConfig)) { + // It is important to base this on the common config, as both the common config and the per-worktree config should be used + cfg = new FileBasedConfig(cfg, worktreeSpecificConfig, safeFS()); + try { + cfg.load(); + } catch (ConfigInvalidException err) { + throw new IllegalArgumentException("Failed to parse config " + worktreeSpecificConfig.getAbsolutePath(), err); + } + } + } + } catch (ConfigInvalidException err) { + throw new IllegalArgumentException("Failed to parse config " + path.getAbsolutePath(), err); + } + return cfg; + } + return super.loadConfig(); + } + + @Override + protected void setupGitDir() throws IOException { + super.setupGitDir(); + + // Setup common directory + if (commonDirectory == null) { + File commonDirFile = safeFS().resolve(getGitDir(), COMMON_DIR); + if (safeFS().exists(commonDirFile) && safeFS().isFile(commonDirFile)) { + byte[] content = IO.readFully(commonDirFile); + if (content.length < 1) { + throw emptyFile(commonDirFile); + } + + int lineEnd = RawParseUtils.nextLF(content, 0); + while (content[lineEnd - 1] == '\n' || (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows())) { + lineEnd--; + } + if (lineEnd <= 1) { + throw emptyFile(commonDirFile); + } + + String commonPath = RawParseUtils.decode(content, 0, lineEnd); + File common = new File(commonPath); + if (common.isAbsolute()) { + commonDirectory = common; + } else { + commonDirectory = safeFS().resolve(getGitDir(), commonPath).getCanonicalFile(); + } + } + } + + // Setup object directory + if (getObjectDirectory() == null) { + setObjectDirectory(resolveWithCommonDir(Constants.OBJECTS)); + } + } + + private static IOException emptyFile(File commonDir) { + return new IOException("Empty 'commondir' file: " + commonDir.getAbsolutePath()); + } + + @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE") + @Override + public FileRepositoryBuilder readEnvironment(SystemReader sr) { + super.readEnvironment(sr); + + // Always overwrite, will trump over the common dir file + String val = sr.getenv(GIT_COMMON_DIR_ENV_KEY); + if (val != null) { + commonDirectory = new File(val); + } + + return self(); + } + + /** + * For repository with multiple linked worktrees some data might be shared in a "common" directory. + * + * @param target the file we want to resolve. + * @return a file resolved from the {@link #getGitDir()}, or possibly in the path specified by $GIT_COMMON_DIR or {@code commondir} file. + */ + File resolveWithCommonDir(String target) { + if (commonDirectory != null) { + return safeFS().resolve(commonDirectory, target); } - } else { - return dir; + return safeFS().resolve(getGitDir(), target); } } } diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/package-info.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Mirror.java similarity index 62% rename from _ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/package-info.java rename to lib-extra/src/main/java/com/diffplug/spotless/extra/P2Mirror.java index b82e4d40ca..172857422b 100644 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/package-info.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/P2Mirror.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Common Spotless Eclipse Equinox setup classes. - * Please refer to the {@code eclipse-base} {@code README.md} for - * usage instructions. - */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.base; +package com.diffplug.spotless.extra; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +@SuppressFBWarnings("UWF_UNWRITTEN_FIELD") +public class P2Mirror { + + private String prefix; + private String url; + + public String getPrefix() { + return prefix; + } -import javax.annotation.ParametersAreNonnullByDefault; + public String getUrl() { + return url; + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java index 4c2b06fc9a..9582eeed31 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,20 @@ */ package com.diffplug.spotless.extra.cpp; -import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; import java.util.Properties; +import com.diffplug.common.collect.ImmutableMap; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder.State; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +import dev.equo.solstice.p2.P2Model; /** * Formatter step which calls out to the Eclipse CDT formatter. - * + *

* Eclipse-CDT org.eclipse.core.contenttype.contentTypes * extension cSource, cHeader, cxxSource and cxxHeader. * can handle: "c", "h", "C", "cpp", "cxx", "cc", "c++", "h", "hpp", "hh", "hxx", "inc" @@ -36,25 +38,40 @@ public final class EclipseCdtFormatterStep { private EclipseCdtFormatterStep() {} private static final String NAME = "eclipse cdt formatter"; - private static final String FORMATTER_CLASS = "com.diffplug.spotless.extra.eclipse.cdt.EclipseCdtFormatterStepImpl"; - private static final String FORMATTER_METHOD = "format"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "4.16.0").add(11, "4.21.0"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(17, "11.6"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } /** Provides default configuration */ - public static EclipseBasedStepBuilder createBuilder(Provisioner provisioner) { - return new EclipseBasedStepBuilder(NAME, provisioner, EclipseCdtFormatterStep::apply); + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, defaultVersion(), EclipseCdtFormatterStep::apply, ImmutableMap.builder()) { + @Override + protected P2Model model(String version) { + var model = new P2Model(); + addPlatformRepo(model, "4.26"); + model.addP2Repo("https://download.eclipse.org/tools/cdt/releases/" + version + "/"); + model.getInstall().add("org.eclipse.cdt.core"); + return model; + } + }; } - private static FormatterFunc apply(State state) throws Exception { + private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion()); - Class formatterClazz = state.loadClass(FORMATTER_CLASS); - Object formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); - Method method = formatterClazz.getMethod(FORMATTER_METHOD, String.class); - return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), input -> (String) method.invoke(formatter, input)); + Class formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.cdt.EclipseCdtFormatterStepImpl"); + var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); + var method = formatterClazz.getMethod("format", String.class); + return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), + input -> { + try { + return (String) method.invoke(formatter, input); + } catch (InvocationTargetException exceptionWrapper) { + Throwable throwable = exceptionWrapper.getTargetException(); + Exception exception = (throwable instanceof Exception) ? (Exception) throwable : null; + throw (null == exception) ? exceptionWrapper : exception; + } + }); } - } 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 013f5b5c2c..4e35dceac5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,16 @@ package com.diffplug.spotless.extra.groovy; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.List; import java.util.Properties; +import com.diffplug.common.collect.ImmutableMap; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder.State; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +import dev.equo.solstice.p2.P2Model; /** Formatter step which calls out to the Groovy-Eclipse formatter. */ public final class GrEclipseFormatterStep { @@ -31,26 +33,62 @@ public final class GrEclipseFormatterStep { private GrEclipseFormatterStep() {} private static final String NAME = "eclipse groovy formatter"; - private static final String FORMATTER_CLASS = "com.diffplug.spotless.extra.eclipse.groovy.GrEclipseFormatterStepImpl"; - private static final String FORMATTER_CLASS_OLD = "com.diffplug.gradle.spotless.groovy.eclipse.GrEclipseFormatterStepImpl"; - private static final String MAVEN_GROUP_ARTIFACT = "com.diffplug.spotless:spotless-eclipse-groovy"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "4.19.0").add(11, "4.21.0"); - private static final String FORMATTER_METHOD = "format"; + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26").add(17, "4.32"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } - /** Provides default configuration */ - public static EclipseBasedStepBuilder createBuilder(Provisioner provisioner) { - return new EclipseBasedStepBuilder(NAME, provisioner, GrEclipseFormatterStep::apply); + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, defaultVersion(), GrEclipseFormatterStep::apply, ImmutableMap.builder()) { + @Override + protected P2Model model(String version) { + if (!version.startsWith("4.")) { + throw new IllegalArgumentException("Expected version 4.x"); + } + int eVersion = Integer.parseInt(version.substring("4.".length())); + if (eVersion < 8) { + throw new IllegalArgumentException("4.8 is the oldest version we support, this was " + version); + } + String greclipseVersion; + if (eVersion >= 28) { + greclipseVersion = "5." + (eVersion - 28) + ".0"; + } else if (eVersion >= 18) { + greclipseVersion = "4." + (eVersion - 18) + ".0"; + } else { + greclipseVersion = "3." + (eVersion - 8) + ".0"; + } + var model = new P2Model(); + addPlatformRepo(model, version); + model.addP2Repo("https://groovy.jfrog.io/artifactory/plugins-release/org/codehaus/groovy/groovy-eclipse-integration/" + greclipseVersion + "/e" + version + "/"); + model.getInstall().addAll(List.of( + "org.codehaus.groovy.eclipse.refactoring", + "org.codehaus.groovy.eclipse.core", + "org.eclipse.jdt.groovy.core", + "org.codehaus.groovy")); + model.addFilterAndValidate("no-debug", filter -> { + filter.exclude("org.eclipse.jdt.debug"); + }); + return model; + } + + @Override + public void setVersion(String version) { + if (version.endsWith(".0")) { + String newVersion = version.substring(0, version.length() - 2); + System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for eclipse JDT"); + version = newVersion; + } + super.setVersion(version); + } + }; } - private static FormatterFunc apply(EclipseBasedStepBuilder.State state) throws Exception { + private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion()); - Class formatterClazz = getClass(state); - Object formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); - Method method = formatterClazz.getMethod(FORMATTER_METHOD, String.class); + Class formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.groovy.GrEclipseFormatterStepImpl"); + var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); + var method = formatterClazz.getMethod("format", String.class); return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), input -> { try { @@ -62,12 +100,4 @@ private static FormatterFunc apply(EclipseBasedStepBuilder.State state) throws E } }); } - - private static Class getClass(State state) { - if (state.getMavenCoordinate(MAVEN_GROUP_ARTIFACT).isPresent()) { - return state.loadClass(FORMATTER_CLASS); - } - return state.loadClass(FORMATTER_CLASS_OLD); - } - } diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java index 15c43c815b..a0d056bc0a 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,11 @@ import java.nio.file.Path; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.Objects; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.MyersDiff; import org.eclipse.jgit.diff.RawText; @@ -56,15 +58,17 @@ interface CleanProvider { } private static class CleanProviderFormatter implements CleanProvider { + private final Path rootDir; private final Formatter formatter; - CleanProviderFormatter(Formatter formatter) { + CleanProviderFormatter(Path rootDir, Formatter formatter) { + this.rootDir = Objects.requireNonNull(rootDir); this.formatter = Objects.requireNonNull(formatter); } @Override public Path getRootDir() { - return formatter.getRootDir(); + return rootDir; } @Override @@ -121,8 +125,8 @@ public Builder runToFix(String runToFix) { return this; } - public Builder formatter(Formatter formatter) { - this.formatter = new CleanProviderFormatter(formatter); + public Builder formatter(Path rootDir, Formatter formatter) { + this.formatter = new CleanProviderFormatter(rootDir, formatter); return this; } @@ -143,7 +147,7 @@ public String getMessage() { Objects.requireNonNull(runToFix, "runToFix"); Objects.requireNonNull(formatter, "formatter"); Objects.requireNonNull(problemFiles, "problemFiles"); - DiffMessageFormatter diffFormater = new DiffMessageFormatter(this); + DiffMessageFormatter diffFormater = new DiffMessageFormatter(formatter, problemFiles); return "The following files had format violations:\n" + diffFormater.buffer + runToFix; @@ -151,10 +155,6 @@ public String getMessage() { throw Errors.asRuntime(e); } } - - String relativePath(File file) { - return formatter.getRootDir().relativize(file.toPath()).toString(); - } } private static final int MAX_CHECK_MESSAGE_LINES = 50; @@ -163,25 +163,32 @@ String relativePath(File file) { private final StringBuilder buffer = new StringBuilder(MAX_CHECK_MESSAGE_LINES * 64); private int numLines = 0; - private DiffMessageFormatter(Builder builder) throws IOException { - ListIterator problemIter = builder.problemFiles.listIterator(); + private final CleanProvider formatter; + + private DiffMessageFormatter(CleanProvider formatter, List problemFiles) throws IOException { + this.formatter = Objects.requireNonNull(formatter, "formatter"); + ListIterator problemIter = problemFiles.listIterator(); while (problemIter.hasNext() && numLines < MAX_CHECK_MESSAGE_LINES) { File file = problemIter.next(); - addFile(builder.relativePath(file) + "\n" + DiffMessageFormatter.diff(builder, file)); + addFile(relativePath(file) + "\n" + diff(file)); } if (problemIter.hasNext()) { - int remainingFiles = builder.problemFiles.size() - problemIter.nextIndex(); + int remainingFiles = problemFiles.size() - problemIter.nextIndex(); if (remainingFiles >= MAX_FILES_TO_LIST) { buffer.append("Violations also present in ").append(remainingFiles).append(" other files.\n"); } else { buffer.append("Violations also present in:\n"); while (problemIter.hasNext()) { - addIntendedLine(NORMAL_INDENT, builder.relativePath(problemIter.next())); + addIntendedLine(NORMAL_INDENT, relativePath(problemIter.next())); } } } } + private String relativePath(File file) { + return formatter.getRootDir().relativize(file.toPath()).toString(); + } + private static final int MIN_LINES_PER_FILE = 4; private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); @@ -230,10 +237,23 @@ private void addIntendedLine(String indent, String line) { * look like if formatted using the given formatter. Does not end with any newline * sequence (\n, \r, \r\n). */ - private static String diff(Builder builder, File file) throws IOException { - String raw = new String(Files.readAllBytes(file.toPath()), builder.formatter.getEncoding()); + private String diff(File file) throws IOException { + return diff(formatter, file).getValue(); + } + + /** + * Returns a map entry with value being a git-style diff between the contents of the given file and what those contents would + * look like if formatted using the given formatter. Does not end with any newline + * sequence (\n, \r, \r\n). The key of the map entry is the 0-based line where the first difference occurred. + */ + public static Map.Entry diff(Path rootDir, Formatter formatter, File file) throws IOException { + return diff(new CleanProviderFormatter(rootDir, formatter), file); + } + + private static Map.Entry diff(CleanProvider formatter, File file) throws IOException { + String raw = new String(Files.readAllBytes(file.toPath()), formatter.getEncoding()); String rawUnix = LineEnding.toUnix(raw); - String formatted = builder.formatter.getFormatted(file, rawUnix); + String formatted = formatter.getFormatted(file, rawUnix); String formattedUnix = LineEnding.toUnix(formatted); if (rawUnix.equals(formattedUnix)) { @@ -245,13 +265,13 @@ private static String diff(Builder builder, File file) throws IOException { } /** - * Returns a git-style diff between the two unix strings. - * + * Returns a map entry with value being a git-style diff between the two unix strings and key being the 0-based line of the first difference (in the dirty string) + *

* Output has no trailing newlines. - * + *

* Boolean args determine whether whitespace or line endings will be visible. */ - private static String diffWhitespaceLineEndings(String dirty, String clean, boolean whitespace, boolean lineEndings) throws IOException { + private static Map.Entry diffWhitespaceLineEndings(String dirty, String clean, boolean whitespace, boolean lineEndings) throws IOException { dirty = visibleWhitespaceLineEndings(dirty, whitespace, lineEndings); clean = visibleWhitespaceLineEndings(clean, whitespace, lineEndings); @@ -268,14 +288,18 @@ private static String diffWhitespaceLineEndings(String dirty, String clean, bool // we don't need the diff to show this, since we display newlines ourselves formatted = formatted.replace("\\ No newline at end of file\n", ""); - return NEWLINE_MATCHER.trimTrailingFrom(formatted); + return Map.entry(getLineOfFirstDifference(edits), NEWLINE_MATCHER.trimTrailingFrom(formatted)); + } + + private static int getLineOfFirstDifference(EditList edits) { + return edits.stream().mapToInt(Edit::getBeginA).min().getAsInt(); } private static final CharMatcher NEWLINE_MATCHER = CharMatcher.is('\n'); /** * Makes the whitespace and/or the lineEndings visible. - * + *

* MyersDiff wants inputs with only unix line endings. So this ensures that that is the case. */ private static String visibleWhitespaceLineEndings(String input, boolean whitespace, boolean lineEndings) { 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 d0d42f2814..396065d69a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,17 @@ package com.diffplug.spotless.extra.java; import java.io.File; -import java.lang.reflect.Method; +import java.util.Map; import java.util.Properties; +import com.diffplug.common.collect.ImmutableMap; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder.State; +import com.diffplug.spotless.SerializedFunction; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +import dev.equo.solstice.p2.P2Model; /** Formatter step which calls out to the Eclipse JDT formatter. */ public final class EclipseJdtFormatterStep { @@ -31,43 +34,75 @@ public final class EclipseJdtFormatterStep { private EclipseJdtFormatterStep() {} private static final String NAME = "eclipse jdt formatter"; - private static final String FORMATTER_CLASS_OLD = "com.diffplug.gradle.spotless.java.eclipse.EclipseFormatterStepImpl"; - private static final String FORMATTER_CLASS = "com.diffplug.spotless.extra.eclipse.java.EclipseJdtFormatterStepImpl"; - private static final String MAVEN_GROUP_ARTIFACT = "com.diffplug.spotless:spotless-eclipse-jdt"; - private static final String FORMATTER_METHOD = "format"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "4.19.0").add(11, "4.21.0"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26").add(17, "4.34"); public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } - /** Provides default configuration */ - public static EclipseBasedStepBuilder createBuilder(Provisioner provisioner) { - return new EclipseBasedStepBuilder(NAME, provisioner, EclipseJdtFormatterStep::apply); + public static EclipseJdtFormatterStep.Builder createBuilder(Provisioner provisioner) { + return new EclipseJdtFormatterStep.Builder(NAME, provisioner, defaultVersion(), EclipseJdtFormatterStep::apply, ImmutableMap.builder()); } - private static FormatterFunc apply(State state) throws Exception { + private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion()); - Class formatterClazz = getClass(state); - Object formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); - FormatterFunc formatterFunc = getFormatterFunc(formatter, formatterClazz); + Class formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.jdt.EclipseJdtFormatterStepImpl"); + var formatter = formatterClazz.getConstructor(Properties.class, Map.class).newInstance(state.getPreferences(), state.getStepProperties()); + var method = formatterClazz.getMethod("format", String.class, File.class); + FormatterFunc formatterFunc = (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file); return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), formatterFunc); } - private static Class getClass(State state) { - if (state.getMavenCoordinate(MAVEN_GROUP_ARTIFACT).isPresent()) { - return state.loadClass(FORMATTER_CLASS); + public static class Builder extends EquoBasedStepBuilder { + private final ImmutableMap.Builder stepProperties; + + Builder( + String formatterName, + Provisioner mavenProvisioner, + String defaultVersion, + SerializedFunction stateToFormatter, + ImmutableMap.Builder stepProperties) { + super(formatterName, mavenProvisioner, defaultVersion, stateToFormatter, stepProperties); + this.stepProperties = stepProperties; + } + + @Override + protected P2Model model(String version) { + var model = new P2Model(); + addPlatformRepo(model, version); + model.getInstall().add("org.eclipse.jdt.core"); + return model; + } + + @Override + public void setVersion(String version) { + if (version.endsWith(".0")) { + String newVersion = version.substring(0, version.length() - 2); + System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for Eclipse JDT"); + version = newVersion; + } + super.setVersion(version); + } + + public void sortMembersDoNotSortFields(boolean doNotSortFields) { + boolean sortAllMembers = !doNotSortFields; + stepProperties.put("sp_cleanup.sort_members_all", String.valueOf(sortAllMembers)); + } + + public void sortMembersEnabled(boolean enabled) { + stepProperties.put("sp_cleanup.sort_members", String.valueOf(enabled)); + } + + public void sortMembersOrder(String order) { + stepProperties.put("outlinesortoption", order); + } + + public void sortMembersVisibilityOrder(String order) { + stepProperties.put("org.eclipse.jdt.ui.visibility.order", order); } - return state.loadClass(FORMATTER_CLASS_OLD); - } - private static FormatterFunc getFormatterFunc(Object formatter, Class formatterClazz) throws NoSuchMethodException, SecurityException { - try { - Method method = formatterClazz.getMethod(FORMATTER_METHOD, String.class, File.class); - return (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file); - } catch (NoSuchMethodException e) { - Method method = formatterClazz.getMethod(FORMATTER_METHOD, String.class); - return input -> (String) method.invoke(formatter, input); + public void sortMembersVisibilityOrderEnabled(boolean enabled) { + stepProperties.put("org.eclipse.jdt.ui.enable.visibility.order", String.valueOf(enabled)); } } } diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.11.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.11.0.lockfile deleted file mode 100644 index 7cad5634fe..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.11.0.lockfile +++ /dev/null @@ -1,21 +0,0 @@ -# Spotless formatter based on CDT version 9.7.0 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.7.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -com.ibm.icu:icu4j:61.1 -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.filebuffers:3.6.500 -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.jface.text:3.15.100 -org.eclipse.platform:org.eclipse.jface:3.15.100 -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_cdt_formatter/v4.12.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.12.0.lockfile deleted file mode 100644 index 6fd639528a..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.12.0.lockfile +++ /dev/null @@ -1,21 +0,0 @@ -# Spotless formatter based on CDT version 9.8.0 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.8.1 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -com.ibm.icu:icu4j:61.2 -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.filebuffers:3.6.600 -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.jface.text:3.15.200 -org.eclipse.platform:org.eclipse.jface:3.16.0 -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_cdt_formatter/v4.13.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.13.0.lockfile deleted file mode 100644 index 1d169ff087..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.13.0.lockfile +++ /dev/null @@ -1,21 +0,0 @@ -# Spotless formatter based on CDT version 9.9.0 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.9.0 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -com.ibm.icu:icu4j:61.2 -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.filebuffers:3.6.700 -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.jface.text:3.15.300 -org.eclipse.platform:org.eclipse.jface:3.17.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_cdt_formatter/v4.14.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.14.0.lockfile deleted file mode 100644 index 43cb98774d..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.14.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 9.10.0 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.10.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:64.2 -net.jcip:jcip-annotations:1.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.filebuffers:3.6.800 -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.jface.text:3.16.100 -org.eclipse.platform:org.eclipse.jface:3.18.0 -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_cdt_formatter/v4.16.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.16.0.lockfile deleted file mode 100644 index 3bf6b1ca01..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.16.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 9.11.1 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.11.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.google.j2objc:j2objc-annotations:1.3 -com.ibm.icu:icu4j:64.2 -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.filebuffers:3.6.1000 -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.jface.text:3.16.300 -org.eclipse.platform:org.eclipse.jface:3.20.0 -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_cdt_formatter/v4.17.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.17.0.lockfile deleted file mode 100644 index 78a6db147f..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.17.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 10.0 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:10.0.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.1 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:64.2 -net.jcip:jcip-annotations:1.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.filebuffers:3.6.1000 -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.jface.text:3.16.400 -org.eclipse.platform:org.eclipse.jface:3.21.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_cdt_formatter/v4.18.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.18.0.lockfile deleted file mode 100644 index 7135a6826d..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.18.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 10.1 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:10.1.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:64.2 -net.jcip:jcip-annotations:1.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.filebuffers:3.6.1100 -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.jface.text:3.16.500 -org.eclipse.platform:org.eclipse.jface:3.22.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_cdt_formatter/v4.19.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.19.0.lockfile deleted file mode 100644 index 7c508e64bd..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.19.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 10.2 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:10.2.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:67.1 -net.jcip:jcip-annotations:1.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.filebuffers:3.6.1100 -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.jface.text:3.17.0 -org.eclipse.platform:org.eclipse.jface:3.22.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_cdt_formatter/v4.20.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.20.0.lockfile deleted file mode 100644 index 6cdb507d00..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.20.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 10.3 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:10.3.0 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:67.1 -net.jcip:jcip-annotations:1.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.filebuffers:3.7.0 -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.jface.text:3.18.0 -org.eclipse.platform:org.eclipse.jface:3.22.200 -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_cdt_formatter/v4.21.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.21.0.lockfile deleted file mode 100644 index 4acd5a98ca..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.21.0.lockfile +++ /dev/null @@ -1,22 +0,0 @@ -# Spotless formatter based on CDT version 10.4 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:10.4.0 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -com.ibm.icu:icu4j:67.1 -net.jcip:jcip-annotations:1.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.filebuffers:3.7.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.jface.text:3.18.100 -org.eclipse.platform:org.eclipse.jface:3.23.0 -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_cdt_formatter/v4.7.3a.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.7.3a.lockfile deleted file mode 100644 index 92a0d32cc7..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_cdt_formatter/v4.7.3a.lockfile +++ /dev/null @@ -1,21 +0,0 @@ -# Spotless formatter based on CDT version 9.4.3 (see https://www.eclipse.org/cdt/) -com.diffplug.spotless:spotless-eclipse-cdt:9.4.5 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -com.ibm.icu:icu4j:61.1 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.filebuffers:3.6.200 -org.eclipse.platform:org.eclipse.core.filesystem:1.7.100 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.jface.text:3.13.0 -org.eclipse.platform:org.eclipse.jface:3.14.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v2.3.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v2.3.0.lockfile deleted file mode 100644 index 878ddbe76a..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v2.3.0.lockfile +++ /dev/null @@ -1,5 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 2.3.0 (see https://github.com/groovy/groovy-eclipse/wiki) -# -# This version is deprecated since the new version system is based on the Eclipse versioning. -# Use the corresponding Eclipse version 4.6.3 instead. -com.diffplug.spotless:spotless-ext-greclipse:2.3.0 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.10.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.10.0.lockfile deleted file mode 100644 index 33f672b306..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.10.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.0.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.2.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.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.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.jface.text:3.15.100 -org.eclipse.platform:org.eclipse.jface:3.15.100 -org.eclipse.platform:org.eclipse.osgi:3.13.300 -org.eclipse.platform:org.eclipse.text:3.8.100 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.12.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.12.0.lockfile deleted file mode 100644 index a60e5e8fb0..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.12.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.4.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.4.0 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.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.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.jface.text:3.15.200 -org.eclipse.platform:org.eclipse.jface:3.16.0 -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_groovy_formatter/v4.13.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.13.0.lockfile deleted file mode 100644 index aede5b0a76..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.13.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.5.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.5.0 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.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.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.jface.text:3.15.300 -org.eclipse.platform:org.eclipse.jface:3.17.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_groovy_formatter/v4.14.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.14.0.lockfile deleted file mode 100644 index bca432c337..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.14.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.6.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.6.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -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.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.jface.text:3.16.100 -org.eclipse.platform:org.eclipse.jface:3.18.0 -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_groovy_formatter/v4.15.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.15.0.lockfile deleted file mode 100644 index fc91fae066..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.15.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.7.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.7.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -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.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.jface.text:3.16.200 -org.eclipse.platform:org.eclipse.jface:3.19.0 -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_groovy_formatter/v4.16.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.16.0.lockfile deleted file mode 100644 index 2c5f4ccd03..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.16.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.8.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -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.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.jface.text:3.16.300 -org.eclipse.platform:org.eclipse.jface:3.20.0 -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_groovy_formatter/v4.17.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.17.0.lockfile deleted file mode 100644 index 5242d0da8c..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.17.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.9.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.9.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.1 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.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.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.jface.text:3.16.400 -org.eclipse.platform:org.eclipse.jface:3.21.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_groovy_formatter/v4.18.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.18.0.lockfile deleted file mode 100644 index 6bfec53756..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.18.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 4.0.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:4.0.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.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.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.jface.text:3.16.500 -org.eclipse.platform:org.eclipse.jface:3.22.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_groovy_formatter/v4.19.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.19.0.lockfile deleted file mode 100644 index 880d55ac8d..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.19.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 4.1.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:4.1.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.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.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.jface.text:3.17.0 -org.eclipse.platform:org.eclipse.jface:3.22.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_groovy_formatter/v4.20.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.20.0.lockfile deleted file mode 100644 index 2836c05982..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.20.0.lockfile +++ /dev/null @@ -1,20 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 4.2.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:4.2.0 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.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.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.jface.text:3.18.0 -org.eclipse.platform:org.eclipse.jface:3.22.200 -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_groovy_formatter/v4.21.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.21.0.lockfile deleted file mode 100644 index 38376d1b32..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.21.0.lockfile +++ /dev/null @@ -1,20 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 4.3.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:4.3.0 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.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.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.jface.text:3.18.100 -org.eclipse.platform:org.eclipse.jface:3.23.0 -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_groovy_formatter/v4.6.3.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.6.3.lockfile deleted file mode 100644 index a6299ec57d..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.6.3.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 2.3.0 (see https://github.com/groovy/groovy-eclipse/wiki) -com.diffplug.spotless:spotless-ext-greclipse:2.3.0 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.0.lockfile deleted file mode 100644 index bb95c71104..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 2.9.2 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:2.9.2 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.jface.text:3.13.0 -org.eclipse.platform:org.eclipse.jface:3.14.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.1.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.1.lockfile deleted file mode 100644 index 9198eaccb9..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter/v4.8.1.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on Groovy-Eclipse version 3.0.0 (see https://github.com/groovy/groovy-eclipse/releases) -com.diffplug.spotless:spotless-eclipse-groovy:3.0.1 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.jface.text:3.13.0 -org.eclipse.platform:org.eclipse.jface:3.14.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.0.lockfile deleted file mode 100644 index 42d81d7324..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.10.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.10.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_10 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.16.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.0.lockfile deleted file mode 100644 index 3bbf59db23..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.11.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.11.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_11 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.17.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 - diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.0.lockfile deleted file mode 100644 index c9bfc9a57d..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.12.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.12.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_12 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.13.0.lockfile deleted file mode 100644 index 4ee0adb3cf..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.13.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.13.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_13 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.2.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.14.0.lockfile deleted file mode 100644 index b42352633a..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.14.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.14.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_14 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.15.0.lockfile deleted file mode 100644 index ee21631962..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.15.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.15.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_15 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.jobs:3.10.700 -org.eclipse.platform:org.eclipse.core.resources:3.13.600 -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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.16.0.lockfile deleted file mode 100644 index 7ff27b440a..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.16.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.16.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_16 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.3.0 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.17.0.lockfile deleted file mode 100644 index 47cb5f10dd..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.17.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.17.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_17 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.1 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.18.0.lockfile deleted file mode 100644 index c5b2799c11..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.18.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.18.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_18 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.19.0.lockfile deleted file mode 100644 index fcc96b8520..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.19.0.lockfile +++ /dev/null @@ -1,19 +0,0 @@ -# Spotless formatter based on JDT version 4.19.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_19 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.4.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.osgi:3.16.200 -org.eclipse.platform:org.eclipse.text:3.11.0 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.0.lockfile deleted file mode 100644 index b06a9bc617..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.20.0.lockfile +++ /dev/null @@ -1,20 +0,0 @@ -# Spotless formatter based on JDT version 4.20.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_20 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.1 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.21.0.lockfile deleted file mode 100644 index 84a90489c9..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.21.0.lockfile +++ /dev/null @@ -1,20 +0,0 @@ -# Spotless formatter based on JDT version 4.21.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_21 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.1 -com.diffplug.spotless:spotless-eclipse-base:3.5.2 -com.github.spotbugs:spotbugs-annotations:4.0.2 -com.google.code.findbugs:jsr305:3.0.2 -net.jcip:jcip-annotations:1.0 -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.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.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.6.1.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.1.lockfile deleted file mode 100644 index fd408e7ecc..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.1.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on JDT version 4.6.1 (see https://projects.eclipse.org/projects/eclipse.jdt) -com.diffplug.spotless:spotless-ext-eclipse-jdt:4.6.1 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.2.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.2.lockfile deleted file mode 100644 index a042b7e734..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.2.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.6.2 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_6_maintenance to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.12.2 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.3.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.3.lockfile deleted file mode 100644 index 714ff708c8..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.6.3.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on JDT version 4.6.3 (see https://projects.eclipse.org/projects/eclipse.jdt) -com.diffplug.spotless:spotless-ext-eclipse-jdt:4.6.3 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.0.lockfile deleted file mode 100644 index 9e3eefbcb1..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.0.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on JDT version 4.7 (see https://projects.eclipse.org/projects/eclipse.jdt) -com.diffplug.spotless:spotless-ext-eclipse-jdt:4.7.0 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.1.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.1.lockfile deleted file mode 100644 index 650c67e13b..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.1.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on JDT version 4.7.1 (see https://projects.eclipse.org/projects/eclipse.jdt) -com.diffplug.spotless:spotless-ext-eclipse-jdt:4.7.1 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.2.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.2.lockfile deleted file mode 100644 index 35abd036aa..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.2.lockfile +++ /dev/null @@ -1,2 +0,0 @@ -# Spotless formatter based on JDT version 4.7.2 (see https://projects.eclipse.org/projects/eclipse.jdt) -com.diffplug.spotless:spotless-ext-eclipse-jdt:4.7.2 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.3a.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.3a.lockfile deleted file mode 100644 index a5e7aaa57b..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.7.3a.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.7.3a (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/log/?h=R4_7_maintenance to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.13.101 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.8.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.8.0.lockfile deleted file mode 100644 index a9204f5a45..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.8.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.8.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_8 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.14.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.0.lockfile b/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.0.lockfile deleted file mode 100644 index 5a19c69254..0000000000 --- a/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter/v4.9.0.lockfile +++ /dev/null @@ -1,18 +0,0 @@ -# Spotless formatter based on JDT version 4.9.0 (see https://projects.eclipse.org/projects/eclipse.jdt) -# Compare tag in M2 pom with https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tag/?h=R4_9 to determine core version. -com.diffplug.spotless:spotless-eclipse-jdt:4.8.0 -com.diffplug.spotless:spotless-eclipse-base:3.1.1 -com.google.code.findbugs:annotations:3.0.0 -com.google.code.findbugs:jsr305:3.0.0 -org.eclipse.jdt:org.eclipse.jdt.core:3.15.0 -org.eclipse.platform:org.eclipse.core.commands:3.9.100 -org.eclipse.platform:org.eclipse.core.contenttype:3.7.0 -org.eclipse.platform:org.eclipse.core.jobs:3.10.0 -org.eclipse.platform:org.eclipse.core.resources:3.13.0 -org.eclipse.platform:org.eclipse.core.runtime:3.14.0 -org.eclipse.platform:org.eclipse.equinox.app:1.3.500 -org.eclipse.platform:org.eclipse.equinox.common:3.10.0 -org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100 -org.eclipse.platform:org.eclipse.equinox.registry:3.8.0 -org.eclipse.platform:org.eclipse.osgi:3.13.0 -org.eclipse.platform:org.eclipse.text:3.6.300 \ No newline at end of file diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java index 35ac58ee85..8af0478543 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,31 +22,35 @@ import java.util.List; import org.assertj.core.api.Assertions; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; import org.junit.jupiter.api.Test; -import com.diffplug.common.base.Errors; import com.diffplug.common.base.StringPrinter; +import com.diffplug.spotless.ClearGitConfig; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ResourceHarness; +@ClearGitConfig class GitAttributesTest extends ResourceHarness { - private List testFiles() { - try { - List result = new ArrayList<>(); - for (String path : TEST_PATHS) { - setFile(path).toContent(""); - result.add(newFile(path)); - } - return result; - } catch (IOException e) { - throw Errors.asRuntime(e); + private List testFiles(String prefix) { + List result = new ArrayList<>(); + for (String path : TEST_PATHS) { + String prefixedPath = prefix + path; + setFile(prefixedPath).toContent(""); + result.add(newFile(prefixedPath)); } + return result; + } + + private List testFiles() { + return testFiles(""); } - private static List TEST_PATHS = Arrays.asList("someFile", "subfolder/someFile", "MANIFEST.MF", "subfolder/MANIFEST.MF"); + private static final List TEST_PATHS = Arrays.asList("someFile", "subfolder/someFile", "MANIFEST.MF", "subfolder/MANIFEST.MF"); @Test - void cacheTest() throws IOException { + void cacheTest() { setFile(".gitattributes").toContent(StringPrinter.buildStringFromLines( "* eol=lf", "*.MF eol=crlf")); @@ -77,7 +81,7 @@ void cacheTest() throws IOException { } @Test - void policyTest() throws IOException { + void policyTest() { setFile(".gitattributes").toContent(StringPrinter.buildStringFromLines( "* eol=lf", "*.MF eol=crlf")); @@ -87,4 +91,54 @@ void policyTest() throws IOException { Assertions.assertThat(policy.getEndingFor(newFile("MANIFEST.MF"))).isEqualTo("\r\n"); Assertions.assertThat(policy.getEndingFor(newFile("subfolder/MANIFEST.MF"))).isEqualTo("\r\n"); } + + @Test + void policyDefaultLineEndingTest() throws GitAPIException { + Git git = Git.init().setDirectory(rootFolder()).call(); + git.close(); + setFile(".git/config").toContent(StringPrinter.buildStringFromLines( + "[core]", + "autocrlf=true", + "eol=lf")); + LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(rootFolder(), () -> testFiles()); + Assertions.assertThat(policy.getEndingFor(newFile("someFile"))).isEqualTo("\r\n"); + } + + @Test + void policyTestWithExternalGitDir() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File gitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(gitDir).call(); + + setFile("project.git/info/attributes").toContent(StringPrinter.buildStringFromLines( + "* eol=lf", + "*.MF eol=crlf")); + LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(projectFolder, () -> testFiles("project/")); + Assertions.assertThat(policy.getEndingFor(newFile("project/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/MANIFEST.MF"))).isEqualTo("\r\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/MANIFEST.MF"))).isEqualTo("\r\n"); + } + + @Test + void policyTestWithCommonDir() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File commonGitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(commonGitDir).call(); + newFolder("project.git/worktrees/"); + + File projectGitDir = newFolder("project.git/worktrees/project/"); + setFile("project.git/worktrees/project/gitdir").toContent(projectFolder.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project/commondir").toContent("../.."); + setFile("project/.git").toContent("gitdir: " + projectGitDir.getAbsolutePath()); + + setFile("project.git/info/attributes").toContent(StringPrinter.buildStringFromLines( + "* eol=lf", + "*.MF eol=crlf")); + LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(projectFolder, () -> testFiles("project/")); + Assertions.assertThat(policy.getEndingFor(newFile("project/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/MANIFEST.MF"))).isEqualTo("\r\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/MANIFEST.MF"))).isEqualTo("\r\n"); + } } 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 02447a36bd..7f2ad99b54 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-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,10 @@ import org.eclipse.jgit.lib.RefDatabase; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ClearGitConfig; import com.diffplug.spotless.ResourceHarness; +@ClearGitConfig class GitRachetMergeBaseTest extends ResourceHarness { @Test void test() throws IllegalStateException, GitAPIException, IOException { diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java new file mode 100644 index 0000000000..b8a1bf8c89 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022-2024 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 java.io.File; +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.ClearGitConfig; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.extra.GitWorkarounds.RepositorySpecificResolver; + +@ClearGitConfig +class GitWorkaroundsTest extends ResourceHarness { + @Test + void inline() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + Git.init().setDirectory(projectFolder).call(); + + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(projectFolder); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(new File(projectFolder, ".git")); + } + + @Test + void external() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File gitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(gitDir).call(); + + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(projectFolder); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(gitDir); + } + + @Nested + @DisplayName("Worktrees") + class Worktrees { + private File project1Tree; + private File project1GitDir; + private File project2Tree; + private File project2GitDir; + private File commonGitDir; + + @BeforeEach + void setUp() throws IOException, GitAPIException { + project1Tree = newFolder("project-w1"); + project2Tree = newFolder("project-w2"); + commonGitDir = newFolder("project.git"); + Git.init().setDirectory(project1Tree).setGitDir(commonGitDir).call(); + + // Setup worktrees manually since JGit does not support it + newFolder("project.git/worktrees/"); + + project1GitDir = newFolder("project.git/worktrees/project-w1/"); + setFile("project.git/worktrees/project-w1/gitdir").toContent(project1Tree.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project-w1/commondir").toContent("../.."); // Relative path + setFile("project-w1/.git").toContent("gitdir: " + project1GitDir.getAbsolutePath()); + + project2GitDir = newFolder("project.git/worktrees/project-w2/"); + setFile("project.git/worktrees/project-w2/gitdir").toContent(project2Tree.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project-w2/commondir").toContent(commonGitDir.getAbsolutePath()); // Absolute path + setFile("project-w2/.git").toContent("gitdir: " + project2GitDir.getAbsolutePath()); + } + + @Test + void resolveGitDir() { + // Test worktree 1 + { + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project1Tree); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project1GitDir); + Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG)); + } + + // Test worktree 2 + { + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project2Tree); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project2GitDir); + Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG)); + } + } + + @Test + void perWorktreeConfig() throws IOException { + setFile("project.git/config").toLines("[core]", "mySetting = true"); + + Assertions.assertThat(getMySetting(project1Tree)).isTrue(); + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + + // Override setting for project 1, but don't enable extension yet + setFile("project.git/worktrees/project-w1/config.worktree").toLines("[core]", "mySetting = false"); + + Assertions.assertThat(getMySetting(project1Tree)).isTrue(); + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + + // Enable extension + setFile("project.git/config").toLines("[core]", "mySetting = true", "[extensions]", "worktreeConfig = true"); + + Assertions.assertThat(getMySetting(project1Tree)).isFalse(); // Should now be overridden by config.worktree + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + } + + private boolean getMySetting(File projectDir) { + return GitWorkarounds.fileRepositoryResolverForProject(projectDir).getRepositoryConfig().getBoolean("core", "mySetting", false); + } + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java index f39673cdc8..c2a1c4703f 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/cpp/EclipseCdtFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,31 +15,32 @@ */ package com.diffplug.spotless.extra.cpp; +import static org.junit.jupiter.api.condition.JRE.JAVA_17; + import java.util.stream.Stream; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import com.diffplug.spotless.Jvm; import com.diffplug.spotless.TestProvisioner; -import com.diffplug.spotless.extra.eclipse.EclipseResourceHarness; - -class EclipseCdtFormatterStepTest extends EclipseResourceHarness { - private final static Jvm.Support JVM_SUPPORT = Jvm. support("Oldest Version").add(8, "4.11.0"); - private final static String INPUT = "#include ;\nint main(int argc, \nchar *argv[]) {}"; - private final static String EXPECTED = "#include ;\nint main(int argc, char *argv[]) {\n}\n"; +import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; +class EclipseCdtFormatterStepTest extends EquoResourceHarness { public EclipseCdtFormatterStepTest() { - super(EclipseCdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()), INPUT, EXPECTED); + super(EclipseCdtFormatterStep.createBuilder(TestProvisioner.mavenCentral())); } @ParameterizedTest @MethodSource + @EnabledForJreRange(min = JAVA_17) void formatWithVersion(String version) throws Exception { - assertFormatted(version); + harnessFor(version).test("main.c", + "#include ;\nint main(int argc, \nchar *argv[]) {}", + "#include ;\nint main(int argc, char *argv[]) {\n}\n"); } private static Stream formatWithVersion() { - return Stream.of(JVM_SUPPORT.getRecommendedFormatterVersion(), EclipseCdtFormatterStep.defaultVersion()); + return Stream.of("11.0", "11.6", EclipseCdtFormatterStep.defaultVersion()); } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EclipseResourceHarness.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EclipseResourceHarness.java index 71cb4b78de..cd4bebd390 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EclipseResourceHarness.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EclipseResourceHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import java.util.Arrays; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.extra.EclipseBasedStepBuilder; /** @@ -43,57 +43,19 @@ */ public class EclipseResourceHarness extends ResourceHarness { private final EclipseBasedStepBuilder stepBuilder; - private final String fileName; - private final String input; - private final String expected; /** * Create harness to be used for several versions of the formatter step * @param builder Eclipse Formatter step builder - * @param unformatted Simple unformatted input - * @param formatted Expected formatted output */ - public EclipseResourceHarness(EclipseBasedStepBuilder builder, String unformatted, String formatted) { - this(builder, "someSourceFile", unformatted, formatted); + public EclipseResourceHarness(EclipseBasedStepBuilder builder) { + this.stepBuilder = builder; } - /** - * Create harness to be used for several versions of the formatter step - * @param builder Eclipse Formatter step builder - * @param sourceFileName File name of the source file - * @param unformatted Simple unformatted input - * @param formatted Expected formatted output - */ - public EclipseResourceHarness(EclipseBasedStepBuilder builder, String sourceFileName, String unformatted, String formatted) { - stepBuilder = builder; - fileName = sourceFileName; - input = unformatted; - expected = formatted; - } - - /** - * Assert that formatting input results in expected output - * @param formatterVersion Formatter version - * @param settingsFiles Formatter settings - * @return Formatted string - */ - protected String assertFormatted(String formatterVersion, File... settingsFiles) throws Exception { - String output = format(formatterVersion, settingsFiles); - assertThat(output).isEqualTo(expected); - return output; - } - - /** - * Formatting input results and returns output - * @param formatterVersion Formatter version - * @param settingsFiles Formatter settings - * @return Formatted string - */ - protected String format(String formatterVersion, File... settingsFiles) throws Exception { - File inputFile = setFile(fileName).toContent(input); + protected StepHarnessWithFile harnessFor(String formatterVersion, File... settingsFiles) throws Exception { stepBuilder.setVersion(formatterVersion); stepBuilder.setPreferences(Arrays.asList(settingsFiles)); FormatterStep step = stepBuilder.build(); - return LineEnding.toUnix(step.format(input, inputFile)); + return StepHarnessWithFile.forStep(this, step); } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EquoResourceHarness.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EquoResourceHarness.java new file mode 100644 index 0000000000..185bbd1326 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/eclipse/EquoResourceHarness.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2024 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.eclipse; + +import java.io.File; +import java.util.Arrays; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +/** + * Provides a common formatter test for all Spotless Eclipse Formatter steps + * provided by lib-extra. + *

+ * The external Spotless Eclipse Formatter Step implementations are responsible + * to test the functionality of the Spotless adaptations for the Eclipse formatter. + * This explicitly includes a check of various forms of valid and invalid input + * to cover all relevant execution paths during the code formatting and internal + * exception handling. + *

+ *

+ * The lib-extra users, like plugin-gradle and plugin-maven are responsible + * to test the correct provision of user settings to the generic + * Spotless Eclipse Formatter steps provided by lib-extra. + *

+ */ +public class EquoResourceHarness extends ResourceHarness { + private final EquoBasedStepBuilder stepBuilder; + + /** + * Create harness to be used for several versions of the formatter step + * @param builder Eclipse Formatter step builder + */ + public EquoResourceHarness(EquoBasedStepBuilder builder) { + stepBuilder = builder; + } + + protected StepHarnessWithFile harnessFor(String formatterVersion, File... settingsFiles) throws Exception { + stepBuilder.setVersion(formatterVersion); + stepBuilder.setPreferences(Arrays.asList(settingsFiles)); + FormatterStep step = stepBuilder.build(); + return StepHarnessWithFile.forStep(this, step); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java new file mode 100644 index 0000000000..e5159a4498 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2024 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.groovy; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.TestProvisioner; + +public class GrEclipseFormatterStepSpecialCaseTest { + /** + * https://github.com/diffplug/spotless/issues/1657 + *

+ * broken: ${parm == null ? "" : "$parm"} + * works: ${parm == null ? "" : "parm"} + */ + @Test + public void issue_1657() { + Assertions.assertThrows(RuntimeException.class, () -> { + StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral()).build()) + .testResourceUnaffected("groovy/greclipse/format/SomeClass.test"); + }); + } + + @Test + public void issue_1657_fixed() { + StepHarness.forStep(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral()).build()) + .testResourceUnaffected("groovy/greclipse/format/SomeClass.fixed"); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java index f1823e5f52..7bfddc8e1c 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,26 +20,25 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import com.diffplug.spotless.Jvm; import com.diffplug.spotless.TestProvisioner; -import com.diffplug.spotless.extra.eclipse.EclipseResourceHarness; +import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; -class GrEclipseFormatterStepTest extends EclipseResourceHarness { - private final static Jvm.Support JVM_SUPPORT = Jvm. support("Oldest Version").add(8, "2.3.0").add(11, "4.17.0"); +public class GrEclipseFormatterStepTest extends EquoResourceHarness { private final static String INPUT = "class F{ def m(){} }"; private final static String EXPECTED = "class F{\n\tdef m(){}\n}"; public GrEclipseFormatterStepTest() { - super(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral()), INPUT, EXPECTED); + super(GrEclipseFormatterStep.createBuilder(TestProvisioner.mavenCentral())); } @ParameterizedTest @MethodSource void formatWithVersion(String version) throws Exception { - assertFormatted(version); + harnessFor(version).test("test.groovy", + "class F{ def m(){} }", "class F{\n\tdef m(){}\n}"); } private static Stream formatWithVersion() { - return Stream.of(JVM_SUPPORT.getRecommendedFormatterVersion(), GrEclipseFormatterStep.defaultVersion()); + return Stream.of("4.18", GrEclipseFormatterStep.defaultVersion()); } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java new file mode 100644 index 0000000000..c842337b3e --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtEqualityTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.TestProvisioner; +import com.diffplug.spotless.ThrowingEx; + +public class EclipseJdtEqualityTest extends ResourceHarness { + @Test + public void test() throws Exception { + var settings1 = setFile("subfolder1/formatter.xml").toResource("java/eclipse/formatter.xml"); + var settings2 = setFile("subfolder2/formatter.xml").toResource("java/eclipse/formatter.xml"); + var step1 = withSettingsFile(settings1); + var step2 = withSettingsFile(settings2); + + Assertions.assertTrue(step1.equals(step2)); + Assertions.assertTrue(step1.hashCode() == step2.hashCode()); + + var serialized1 = toBytes(step1); + var serialized2 = toBytes(step2); + Assertions.assertFalse(serialized1.equals(serialized2)); + } + + private static FormatterStep withSettingsFile(File settingsFile) { + var builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.setPreferences(List.of(settingsFile)); + return builder.build(); + } + + private static byte[] toBytes(Serializable obj) { + ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); + try (ObjectOutputStream objectOutput = new ObjectOutputStream(byteOutput)) { + objectOutput.writeObject(obj); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + return byteOutput.toByteArray(); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java new file mode 100644 index 0000000000..027e562291 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStepSpecialCaseTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016-2024 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.File; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.TestProvisioner; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +class EclipseJdtFormatterStepSpecialCaseTest { + /** https://github.com/diffplug/spotless/issues/1638 */ + @Test + void issue_1638() { + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("eclipse_formatter_issue_1638.xml").getFile()); + EquoBasedStepBuilder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.setPreferences(List.of(file)); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/AbstractType.test", "java/eclipse/AbstractType.clean"); + } + + @Test + void sort_members_global_by_visibility() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(false); + builder.sortMembersVisibilityOrderEnabled(true); + builder.sortMembersVisibilityOrder("B,R,D,V"); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.test", "java/eclipse/SortExample.sortMembersByVisibility.clean"); + } + + @Test + void sort_members_global_enabled() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(false); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.test", "java/eclipse/SortExample.sortMembers.clean"); + } + + @Test + void sort_members_global_no_fields() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(true); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.test", "java/eclipse/SortExample.sortMembersNoFields.clean"); + } + + @Test + void sort_members_local_by_visibility() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(false); + builder.sortMembersVisibilityOrderEnabled(true); + builder.sortMembersVisibilityOrder("B,R,D,V"); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.localSortByVisibility.test", "java/eclipse/SortExample.localSortByVisibility.clean"); + } + + @Test + void sort_members_local_enabled_false() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(false); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.localEnabledFalse.test", "java/eclipse/SortExample.localEnabledFalse.clean"); + } + + @Test + void sort_members_local_no_fields() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + builder.sortMembersEnabled(true); + builder.sortMembersOrder("SF,SI,SM,F,I,C,M,T"); + builder.sortMembersDoNotSortFields(false); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.localDoNotSortFields.test", "java/eclipse/SortExample.localDoNotSortFields.clean"); + } + + @Test + void sort_members_local_enabled_true() { + EclipseJdtFormatterStep.Builder builder = EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); + StepHarness.forStep(builder.build()) + .testResource("java/eclipse/SortExample.localEnabledTrue.test", "java/eclipse/SortExample.localEnabledTrue.clean"); + } +} 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 3e5e1853c7..dbbfdb04a0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,58 +15,49 @@ */ package com.diffplug.spotless.extra.java; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; - -import java.io.File; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import com.diffplug.spotless.Jvm; import com.diffplug.spotless.TestProvisioner; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; -import com.diffplug.spotless.extra.eclipse.EclipseResourceHarness; - -class EclipseJdtFormatterStepTest extends EclipseResourceHarness { - private final static String NON_SEMANTIC_ECLIPSE_VERSION = "4.7.3a"; - private final static Jvm.Support JVM_SUPPORT = Jvm. support("Oldest Version").add(8, "4.6.1").add(11, "4.20.0"); - private final static String INPUT = "package p; class C{}"; - private final static String EXPECTED = "package p;\nclass C {\n}"; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; - private static EclipseBasedStepBuilder createBuilder() { +class EclipseJdtFormatterStepTest extends EquoResourceHarness { + private static EquoBasedStepBuilder createBuilder() { return EclipseJdtFormatterStep.createBuilder(TestProvisioner.mavenCentral()); } public EclipseJdtFormatterStepTest() { - super(createBuilder(), INPUT, EXPECTED); + super(createBuilder()); } @ParameterizedTest @MethodSource void formatWithVersion(String version) throws Exception { - assertFormatted(version); + harnessFor(version).test("test.java", + "package p; class C{}", + "package p;\nclass C {\n}"); } private static Stream formatWithVersion() { - return Stream.of(NON_SEMANTIC_ECLIPSE_VERSION, JVM_SUPPORT.getRecommendedFormatterVersion(), EclipseJdtFormatterStep.defaultVersion()); + return Stream.of("4.9", EclipseJdtFormatterStep.defaultVersion()); } /** New format interface requires source file information to distinguish module-info from compilation unit */ @Nested - @EnabledForJreRange(min = JAVA_11) - class NewFormatInterface extends EclipseResourceHarness { - public NewFormatInterface() throws Exception { - super(createBuilder(), "module-info.java", getTestResource("java/eclipse/ModuleInfoUnformatted.test"), getTestResource("java/eclipse/ModuleInfoFormatted.test")); + class NewFormatInterface extends EquoResourceHarness { + public NewFormatInterface() { + super(createBuilder()); } @Test void formatModuleInfo() throws Exception { - File settingsFile = createTestFile("java/eclipse/ModuleInfo.prefs"); - assertFormatted(JVM_SUPPORT.getRecommendedFormatterVersion(), settingsFile); + harnessFor("4.11", createTestFile("java/eclipse/ModuleInfo.prefs")) + .testResource("module-info.java", "java/eclipse/ModuleInfoUnformatted.test", "java/eclipse/ModuleInfoFormatted.test"); } } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStepTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStepTest.java index 9517c24121..e4a2b7cc0e 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStepTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package com.diffplug.spotless.extra.wtp; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -38,14 +36,18 @@ public class EclipseWtpFormatterStepTest { private final static Jvm.Support JVM_SUPPORT = Jvm. support("Oldest Version").add(8, "4.8.0"); private static class NestedTests extends EclipseResourceHarness { + private final String unformatted, formatted; + public NestedTests(String unformatted, String formatted, EclipseWtpFormatterStep kind) { - super(kind.createBuilder(TestProvisioner.mavenCentral()), unformatted, formatted); + super(kind.createBuilder(TestProvisioner.mavenCentral())); + this.unformatted = unformatted; + this.formatted = formatted; } @ParameterizedTest @MethodSource void formatWithVersion(String version) throws Exception { - assertFormatted(version); + harnessFor(version).test("someFilename", unformatted, formatted); } private static Stream formatWithVersion() { @@ -67,8 +69,8 @@ void multipleConfigurations() throws Exception { config.setProperty("indentationChar", "space"); config.setProperty("indentationSize", "5"); }); - String defaultFormatted = assertFormatted(EclipseWtpFormatterStep.defaultVersion(), tabPropertyFile); - assertThat(format(EclipseWtpFormatterStep.defaultVersion(), spacePropertyFile)).as("Space formatting output unexpected").isEqualTo(defaultFormatted.replace("\t", " ")); + harnessFor(EclipseWtpFormatterStep.defaultVersion(), tabPropertyFile).test("someFilename", unformatted, formatted); + harnessFor(EclipseWtpFormatterStep.defaultVersion(), spacePropertyFile).test("someFilename", unformatted, formatted.replace("\t", " ")); } private File createPropertyFile(Consumer config) throws IOException { diff --git a/lib-extra/src/test/resources/eclipse_formatter_issue_1638.xml b/lib-extra/src/test/resources/eclipse_formatter_issue_1638.xml new file mode 100644 index 0000000000..583df63026 --- /dev/null +++ b/lib-extra/src/test/resources/eclipse_formatter_issue_1638.xml @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/build.gradle b/lib/build.gradle index 90d985ad69..fcf59cd414 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id 'io.github.davidburstrom.version-compatibility' } ext.artifactId = project.artifactIdLib version = rootProject.spotlessChangelog.versionNext @@ -7,10 +8,20 @@ apply from: rootProject.file('gradle/java-setup.gradle') apply from: rootProject.file('gradle/java-publish.gradle') def NEEDS_GLUE = [ - 'sortPom', - 'palantirJavaFormat', + // (alphabetic order please) + 'cleanthat', + 'diktat', + 'flexmark', + 'gherkin', + 'googleJavaFormat', + 'gson', + 'jackson', + 'ktfmt', 'ktlint', - 'flexmark' + 'palantirJavaFormat', + 'scalafmt', + 'sortPom', + 'zjsonPatch', ] for (glue in NEEDS_GLUE) { sourceSets.register(glue) { @@ -20,31 +31,122 @@ for (glue in NEEDS_GLUE) { } } -dependencies { - // zero runtime reqs is a hard requirements for spotless-lib - // if you need a dep, put it in lib-extra - testImplementation "org.junit.jupiter:junit-jupiter:$VER_JUNIT" - testImplementation "org.assertj:assertj-core:$VER_ASSERTJ" - testImplementation "com.diffplug.durian:durian-testlib:$VER_DURIAN" +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 = [ + '0.48.0', + '0.49.0', + '0.50.0', + '1.0.0', + ] + targetSourceSetName = 'ktlint' + } + namespaces.register('Diktat') { + versions = [ + '1.2.5', + '2.0.0', + ] + targetSourceSetName = 'diktat' + } + } +} - // used for pom sorting - sortPomCompileOnly 'com.github.ekryd.sortpom:sortpom-sorter:3.0.0' +tasks.named("check").configure { + dependsOn(tasks.named("testCompatibilityAdapters")) + dependsOn(tasks.named("testCompatibility")) +} - palantirJavaFormatCompileOnly 'com.palantir.javaformat:palantir-java-format:1.1.0' +dependencies { + compileOnly 'org.slf4j:slf4j-api:2.0.17' + testCommonImplementation 'org.slf4j:slf4j-api:2.0.17' - String VER_KTLINT='0.43.2' - ktlintCompileOnly "com.pinterest:ktlint:$VER_KTLINT" - ktlintCompileOnly "com.pinterest.ktlint:ktlint-core:$VER_KTLINT" - ktlintCompileOnly "com.pinterest.ktlint:ktlint-ruleset-standard:$VER_KTLINT" + // zero runtime reqs is a hard requirements for spotless-lib + // if you need a dep, put it in lib-extra + testCommonImplementation "org.junit.jupiter:junit-jupiter:$VER_JUNIT" + testCommonImplementation "org.assertj:assertj-core:$VER_ASSERTJ" + testCommonImplementation "com.diffplug.durian:durian-testlib:$VER_DURIAN" + testCommonRuntimeOnly "org.junit.platform:junit-platform-launcher" - // used for markdown formatting - flexmarkCompileOnly 'com.vladsch.flexmark:flexmark-all:0.62.2' + // GLUE CODE (alphabetic order please) + // cleanthat + String VER_CLEANTHAT='2.22' + cleanthatCompileOnly "io.github.solven-eu.cleanthat:java:$VER_CLEANTHAT" + compatCleanthat2Dot1CompileAndTestOnly "io.github.solven-eu.cleanthat:java:$VER_CLEANTHAT" + // diktat old supported version 1.x + compatDiktat1Dot2Dot5CompileOnly "org.cqfn.diktat:diktat-rules:1.2.5" + // diktat latest supported version 2.x + compatDiktat2Dot0Dot0CompileOnly "com.saveourtool.diktat:diktat-runner:2.0.0" + // flexmark + flexmarkCompileOnly 'com.vladsch.flexmark:flexmark-all:0.64.8' + // gherkin + gherkinCompileOnly 'io.cucumber:gherkin-utils:9.0.0' + gherkinCompileOnly 'org.slf4j:slf4j-api:2.0.17' + // googleJavaFormat + googleJavaFormatCompileOnly 'com.google.googlejavaformat:google-java-format:1.24.0' + // gson + gsonCompileOnly 'com.google.code.gson:gson:2.11.0' + // jackson + String VER_JACKSON='2.18.1' + jacksonCompileOnly "com.fasterxml.jackson.core:jackson-databind:$VER_JACKSON" + jacksonCompileOnly "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$VER_JACKSON" + // ktfmt + ktfmtCompileOnly "com.facebook:ktfmt:0.53" + ktfmtCompileOnly("com.google.googlejavaformat:google-java-format") { + version { + strictly '1.7' // for JDK 8 compatibility + } + } + // ktlint oldest supported version + compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-core:0.48.0' + compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-experimental:0.48.0' + compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.48.0' + // ktlint previous supported version + compatKtLint0Dot49Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:0.49.0' + compatKtLint0Dot49Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.49.0' + compatKtLint0Dot49Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.17' + // ktlint previous supported version + compatKtLint0Dot50Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:0.50.0' + compatKtLint0Dot50Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.50.0' + compatKtLint0Dot50Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.17' + // ktlint latest supported version + compatKtLint1Dot0Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:1.0.0' + compatKtLint1Dot0Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:1.0.0' + compatKtLint1Dot0Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.17' + // palantirJavaFormat + palantirJavaFormatCompileOnly 'com.palantir.javaformat:palantir-java-format:1.1.0' // this version needs to stay compilable against Java 8 for CI Job testNpm + // scalafmt + scalafmtCompileOnly "org.scalameta:scalafmt-core_2.13:3.8.1" + // sortPom + sortPomCompileOnly 'com.github.ekryd.sortpom:sortpom-sorter:4.0.0' + sortPomCompileOnly 'org.slf4j:slf4j-api:2.0.17' + // zjsonPatch + zjsonPatchCompileOnly 'com.flipkart.zjsonpatch:zjsonpatch:0.4.16' } // we'll hold the core lib to a high standard -spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes) +spotbugs { + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = com.github.spotbugs.snom.Confidence.valueOf('LOW') +} -test { useJUnitPlatform() } +apply from: rootProject.file('gradle/special-tests.gradle') +tasks.withType(Test).configureEach { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) { + // https://docs.gradle.org/7.5/userguide/upgrading_version_7.html#removes_implicit_add_opens_for_test_workers + jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED" + } +} jar { for (glue in NEEDS_GLUE) { diff --git a/lib/src/cleanthat/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFunc.java b/lib/src/cleanthat/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFunc.java new file mode 100644 index 0000000000..abf55ba936 --- /dev/null +++ b/lib/src/cleanthat/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFunc.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2024 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.java; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.FormatterFunc; + +import eu.solven.cleanthat.config.pojo.CleanthatEngineProperties; +import eu.solven.cleanthat.config.pojo.SourceCodeProperties; +import eu.solven.cleanthat.engine.java.IJdkVersionConstants; +import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorer; +import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorerProperties; +import eu.solven.cleanthat.formatter.LineEnding; +import eu.solven.cleanthat.formatter.PathAndContent; + +/** + * The glue for CleanThat: it is build over the version in build.gradle, but at runtime it will be executed over + * the version loaded in JarState, which is by default defined in com.diffplug.spotless.java.CleanthatJavaStep#JVM_SUPPORT + */ +public class JavaCleanthatRefactorerFunc implements FormatterFunc.NeedsFile { + private static final Logger LOGGER = LoggerFactory.getLogger(JavaCleanthatRefactorerFunc.class); + + private String jdkVersion; + private List included; + private List excluded; + private boolean includeDraft; + + public JavaCleanthatRefactorerFunc(String jdkVersion, List included, List excluded, boolean includeDraft) { + this.jdkVersion = jdkVersion == null ? IJdkVersionConstants.JDK_8 : jdkVersion; + this.included = included == null ? Collections.emptyList() : included; + this.excluded = excluded == null ? Collections.emptyList() : excluded; + this.includeDraft = includeDraft; + } + + public JavaCleanthatRefactorerFunc() { + this(IJdkVersionConstants.JDK_8, Arrays.asList("SafeAndConsensual"), Arrays.asList(), false); + } + + @Override + public String applyWithFile(String unix, File file) throws Exception { + // https://stackoverflow.com/questions/1771679/difference-between-threads-context-class-loader-and-normal-classloader + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + try { + // Ensure CleanThat main Thread has its custom classLoader while executing its refactoring + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + return doApply(unix, file); + } finally { + // Restore the originalClassLoader + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + private String doApply(String input, File file) throws IOException { + // call some API that uses reflection without taking ClassLoader param + CleanthatEngineProperties engineProperties = CleanthatEngineProperties.builder().engineVersion(jdkVersion).build(); + + // Spotless will push us LF content + engineProperties.setSourceCode(SourceCodeProperties.builder().lineEnding(LineEnding.LF).build()); + + JavaRefactorerProperties refactorerProperties = new JavaRefactorerProperties(); + + refactorerProperties.setIncluded(included); + refactorerProperties.setExcluded(excluded); + + refactorerProperties.setIncludeDraft(includeDraft); + + JavaRefactorer refactorer = new JavaRefactorer(engineProperties, refactorerProperties); + + LOGGER.debug("Processing sourceJdk={} included={} excluded={}", jdkVersion, included, excluded, includeDraft); + LOGGER.debug("Available mutators: {}", JavaRefactorer.getAllIncluded()); + + PathAndContent pathAndContent = new PathAndContent(file.toPath(), input); + + return refactorer.doFormat(pathAndContent); + } + +} diff --git a/lib/src/compatDiktat1Dot2Dot5/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat1Dot2Dot5Adapter.java b/lib/src/compatDiktat1Dot2Dot5/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat1Dot2Dot5Adapter.java new file mode 100644 index 0000000000..ff27e96d47 --- /dev/null +++ b/lib/src/compatDiktat1Dot2Dot5/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat1Dot2Dot5Adapter.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023-2024 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.diktat.compat; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider; + +import com.pinterest.ktlint.core.KtLint; +import com.pinterest.ktlint.core.LintError; +import com.pinterest.ktlint.core.RuleSet; +import com.pinterest.ktlint.core.api.EditorConfigOverride; + +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + +public class DiktatCompat1Dot2Dot5Adapter implements DiktatCompatAdapter { + private final List ruleSets; + private final Function2 formatterCallback; + private final ArrayList errors = new ArrayList<>(); + + public DiktatCompat1Dot2Dot5Adapter(@Nullable File configFile) { + if (configFile != null) { + System.setProperty("diktat.config.path", configFile.getAbsolutePath()); + } + this.ruleSets = Collections.singletonList(new DiktatRuleSetProvider().get()); + this.formatterCallback = new FormatterCallback(errors); + } + + static class FormatterCallback implements Function2 { + private final ArrayList errors; + + FormatterCallback(ArrayList errors) { + this.errors = errors; + } + + @Override + public Unit invoke(LintError lintError, Boolean corrected) { + if (!corrected) { + errors.add(lintError); + } + return null; + } + } + + @Override + public String format(final File file, final String content, final boolean isScript) { + errors.clear(); + String result = KtLint.INSTANCE.format(new KtLint.ExperimentalParams( + // Unlike Ktlint, Diktat requires full path to the file. + // See https://github.com/diffplug/spotless/issues/1189, https://github.com/analysis-dev/diktat/issues/1202 + file.getAbsolutePath(), + content, + ruleSets, + Collections.emptyMap(), + formatterCallback, + isScript, + null, + false, + new EditorConfigOverride(), + false)); + DiktatReporting.reportIfRequired(errors, LintError::getLine, LintError::getRuleId, LintError::getDetail); + return result; + } +} diff --git a/lib/src/compatDiktat2Dot0Dot0/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat2Dot0Dot0Adapter.java b/lib/src/compatDiktat2Dot0Dot0/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat2Dot0Dot0Adapter.java new file mode 100644 index 0000000000..b0625b82bb --- /dev/null +++ b/lib/src/compatDiktat2Dot0Dot0/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompat2Dot0Dot0Adapter.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023-2024 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.diktat.compat; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.saveourtool.diktat.DiktatFactoriesKt; +import com.saveourtool.diktat.DiktatProcessor; +import com.saveourtool.diktat.api.DiktatCallback; +import com.saveourtool.diktat.api.DiktatError; +import com.saveourtool.diktat.api.DiktatRuleConfig; +import com.saveourtool.diktat.api.DiktatRuleSet; + +import kotlin.Unit; + +public class DiktatCompat2Dot0Dot0Adapter implements DiktatCompatAdapter { + private final DiktatProcessor processor; + private final DiktatCallback formatterCallback; + private final ArrayList errors = new ArrayList<>(); + + public DiktatCompat2Dot0Dot0Adapter(@Nullable File configFile) { + this.processor = getDiktatReporter(configFile); + this.formatterCallback = new FormatterCallback(errors); + } + + @Override + public String format(File file, String content, boolean isScript) { + errors.clear(); + String result = processor.fix(content, file.toPath(), formatterCallback); + DiktatReporting.reportIfRequired(errors, DiktatError::getLine, DiktatError::getRuleId, DiktatError::getDetail); + return result; + } + + private static class FormatterCallback implements DiktatCallback { + private final ArrayList errors; + + FormatterCallback(ArrayList errors) { + this.errors = errors; + } + + @Override + public Unit invoke(DiktatError diktatError, Boolean corrected) { + doInvoke(diktatError, corrected); + return Unit.INSTANCE; + } + + @Override + public void invoke(@NotNull DiktatError diktatError, boolean corrected) { + doInvoke(diktatError, corrected); + } + + private void doInvoke(@NotNull DiktatError diktatError, boolean corrected) { + if (!corrected) { + errors.add(diktatError); + } + } + } + + private static DiktatProcessor getDiktatReporter(File configFile) { + final DiktatRuleSet ruleSet = DiktatFactoriesKt.getDiktatRuleSetFactory().invoke(readRuleConfigs(configFile)); + return DiktatFactoriesKt.getDiktatProcessorFactory().invoke(ruleSet); + } + + private static List readRuleConfigs(File configFile) { + if (configFile == null) { + return Collections.emptyList(); + } + try (final InputStream configInputStream = new FileInputStream(configFile)) { + return DiktatFactoriesKt.getDiktatRuleConfigReader().invoke(configInputStream); + } catch (IOException e) { + throw new IllegalArgumentException("Fail to read configFile", e); + } + } +} diff --git a/_ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/package-info.java b/lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompatAdapter.java similarity index 73% rename from _ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/package-info.java rename to lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompatAdapter.java index 831ab2b427..fdc6e77afa 100644 --- a/_ext/eclipse-cdt/src/main/java/com/diffplug/spotless/extra/eclipse/cdt/package-info.java +++ b/lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatCompatAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Eclipse CDT based Spotless formatter */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.cdt; +package com.diffplug.spotless.glue.diktat.compat; -import javax.annotation.ParametersAreNonnullByDefault; +import java.io.File; + +public interface DiktatCompatAdapter { + String format(File file, String content, boolean isScript); +} diff --git a/lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatReporting.java b/lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatReporting.java new file mode 100644 index 0000000000..b51c9508a6 --- /dev/null +++ b/lib/src/compatDiktatApi/java/com/diffplug/spotless/glue/diktat/compat/DiktatReporting.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2024 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.diktat.compat; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public interface DiktatReporting { + class Lint implements Serializable { + private static final long serialVersionUID = 1L; + public final int line; + public final String ruleId; + public final String detail; + + Lint(int line, String ruleId, String detail) { + this.line = line; + this.ruleId = ruleId; + this.detail = detail; + } + } + + class LintException extends RuntimeException { + public final List lints; + + LintException(List lints) { + this.lints = lints; + } + } + + static void reportIfRequired( + List errors, + ToIntFunction lineGetter, + Function codeGetter, + Function detailGetter) { + if (!errors.isEmpty()) { + var lints = new ArrayList(); + for (T er : errors) { + lints.add(new Lint(lineGetter.applyAsInt(er), codeGetter.apply(er), detailGetter.apply(er))); + } + throw new LintException(lints); + } + } +} diff --git a/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java b/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java new file mode 100644 index 0000000000..78bd28f86b --- /dev/null +++ b/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java @@ -0,0 +1,157 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.pinterest.ktlint.core.KtLintRuleEngine; +import com.pinterest.ktlint.core.LintError; +import com.pinterest.ktlint.core.Rule; +import com.pinterest.ktlint.core.RuleProvider; +import com.pinterest.ktlint.core.RuleSetProviderV2; +import com.pinterest.ktlint.core.api.EditorConfigDefaults; +import com.pinterest.ktlint.core.api.EditorConfigOverride; +import com.pinterest.ktlint.core.api.UsesEditorConfigProperties; +import com.pinterest.ktlint.core.api.editorconfig.CodeStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.DisabledRulesEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.EditorConfigProperty; +import com.pinterest.ktlint.core.api.editorconfig.IndentSizeEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.IndentStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt; +import com.pinterest.ktlint.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt; + +import kotlin.Pair; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + +public class KtLintCompat0Dot48Dot0Adapter implements KtLintCompatAdapter { + + private static final List> DEFAULT_EDITOR_CONFIG_PROPERTIES; + + static { + //noinspection deprecation + DEFAULT_EDITOR_CONFIG_PROPERTIES = List.of( + CodeStyleEditorConfigPropertyKt.getCODE_STYLE_PROPERTY(), + DisabledRulesEditorConfigPropertyKt.getDISABLED_RULES_PROPERTY(), + DisabledRulesEditorConfigPropertyKt.getKTLINT_DISABLED_RULES_PROPERTY(), + IndentStyleEditorConfigPropertyKt.getINDENT_STYLE_PROPERTY(), + IndentSizeEditorConfigPropertyKt.getINDENT_SIZE_PROPERTY(), + InsertFinalNewLineEditorConfigPropertyKt.getINSERT_FINAL_NEWLINE_PROPERTY(), + MaxLineLengthEditorConfigPropertyKt.getMAX_LINE_LENGTH_PROPERTY()); + } + + static class FormatterCallback implements Function2 { + @Override + public Unit invoke(LintError lint, Boolean corrected) { + if (!corrected) { + KtLintCompatReporting.report(lint.getLine(), lint.getCol(), lint.getRuleId(), lint.getDetail()); + } + return Unit.INSTANCE; + } + } + + @Override + public String format( + String unix, + Path path, + Path editorConfigPath, + Map editorConfigOverrideMap) { + final FormatterCallback formatterCallback = new FormatterCallback(); + + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV2.class, RuleSetProviderV2.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); + + EditorConfigOverride editorConfigOverride; + if (editorConfigOverrideMap.isEmpty()) { + editorConfigOverride = EditorConfigOverride.Companion.getEMPTY_EDITOR_CONFIG_OVERRIDE(); + } else { + editorConfigOverride = createEditorConfigOverride(allRuleProviders.stream().map( + RuleProvider::createNewRuleInstance).collect( + Collectors.toList()), + editorConfigOverrideMap); + } + EditorConfigDefaults editorConfig; + if (editorConfigPath == null || !Files.exists(editorConfigPath)) { + editorConfig = EditorConfigDefaults.Companion.getEMPTY_EDITOR_CONFIG_DEFAULTS(); + } else { + editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath); + } + + return new KtLintRuleEngine( + allRuleProviders, + editorConfig, + editorConfigOverride, + false) + .format(unix, path, formatterCallback); + } + + /** + * Create EditorConfigOverride from user provided parameters. + */ + private static EditorConfigOverride createEditorConfigOverride(final List rules, Map editorConfigOverrideMap) { + // Get properties from rules in the rule sets + Stream> ruleProperties = rules.stream() + .filter(rule -> rule instanceof UsesEditorConfigProperties) + .flatMap(rule -> ((UsesEditorConfigProperties) rule).getEditorConfigProperties().stream()); + + // Create a mapping of properties to their names based on rule properties and default properties + Map> supportedProperties = Stream + .concat(ruleProperties, DEFAULT_EDITOR_CONFIG_PROPERTIES.stream()) + .distinct() + .collect(Collectors.toMap(EditorConfigProperty::getName, property -> property)); + + // Create config properties based on provided property names and values + @SuppressWarnings("unchecked") + Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() + .map(entry -> { + EditorConfigProperty property = supportedProperties.get(entry.getKey()); + + if (property == null && entry.getKey().startsWith("ktlint_")) { + String[] parts = entry.getKey().substring(7).split("_", 2); + if (parts.length == 1) { + // convert ktlint_{ruleset} to {ruleset} + String qualifiedRuleId = parts[0] + ":"; + property = RuleExecutionEditorConfigPropertyKt.createRuleSetExecutionEditorConfigProperty(qualifiedRuleId); + } else { + // convert ktlint_{ruleset}_{rulename} to {ruleset}:{rulename} + String qualifiedRuleId = parts[0] + ":" + parts[1]; + property = RuleExecutionEditorConfigPropertyKt.createRuleExecutionEditorConfigProperty(qualifiedRuleId); + } + } + + if (property == null) { + return null; + } else { + return new Pair<>(property, entry.getValue()); + } + }) + .filter(Objects::nonNull) + .toArray(Pair[]::new); + + return EditorConfigOverride.Companion.from(properties); + } +} diff --git a/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java b/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java new file mode 100644 index 0000000000..f4c1e04af7 --- /dev/null +++ b/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3; +import com.pinterest.ktlint.rule.engine.api.Code; +import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults; +import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride; +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine; +import com.pinterest.ktlint.rule.engine.api.LintError; +import com.pinterest.ktlint.rule.engine.core.api.Rule; +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider; +import com.pinterest.ktlint.rule.engine.core.api.RuleProviderKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EndOfLinePropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentSizeEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt; + +import kotlin.Pair; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + +public class KtLintCompat0Dot49Dot0Adapter implements KtLintCompatAdapter { + + private static final Logger logger = LoggerFactory.getLogger(KtLintCompat0Dot49Dot0Adapter.class); + + private static final List> DEFAULT_EDITOR_CONFIG_PROPERTIES; + + static { + DEFAULT_EDITOR_CONFIG_PROPERTIES = List.of( + CodeStyleEditorConfigPropertyKt.getCODE_STYLE_PROPERTY(), + EndOfLinePropertyKt.getEND_OF_LINE_PROPERTY(), + IndentSizeEditorConfigPropertyKt.getINDENT_SIZE_PROPERTY(), + IndentStyleEditorConfigPropertyKt.getINDENT_STYLE_PROPERTY(), + InsertFinalNewLineEditorConfigPropertyKt.getINSERT_FINAL_NEWLINE_PROPERTY(), + MaxLineLengthEditorConfigPropertyKt.getMAX_LINE_LENGTH_PROPERTY(), + RuleExecutionEditorConfigPropertyKt.getEXPERIMENTAL_RULES_EXECUTION_PROPERTY()); + } + + private static final Method RULEID_METHOD; + private static final Method CREATE_RULESET_EXECUTION_METHOD; + private static final Method CREATE_RULE_EXECUTION_METHOD; + + static { + try { + RULEID_METHOD = LintError.class.getMethod("getRuleId-6XN97os"); + CREATE_RULESET_EXECUTION_METHOD = RuleExecutionEditorConfigPropertyKt.class.getMethod("createRuleSetExecutionEditorConfigProperty-fqiwTpU", String.class, RuleExecution.class); + CREATE_RULE_EXECUTION_METHOD = RuleExecutionEditorConfigPropertyKt.class.getMethod("createRuleExecutionEditorConfigProperty-U7AdEiY", String.class, RuleExecution.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + private static String getRuleId(LintError lint) { + try { + return (String) RULEID_METHOD.invoke(lint); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static EditorConfigProperty createRuleSetExecution(String id, RuleExecution execution) { + try { + return (EditorConfigProperty) CREATE_RULESET_EXECUTION_METHOD.invoke(null, id, execution); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static EditorConfigProperty createRuleExecution(String id, RuleExecution execution) { + try { + return (EditorConfigProperty) CREATE_RULE_EXECUTION_METHOD.invoke(null, id, execution); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + static class FormatterCallback implements Function2 { + + @Override + public Unit invoke(LintError lint, Boolean corrected) { + if (!corrected) { + KtLintCompatReporting.report(lint.getLine(), lint.getCol(), getRuleId(lint), lint.getDetail()); + } + return Unit.INSTANCE; + } + } + + @Override + public String format( + String unix, + Path path, + Path editorConfigPath, + Map editorConfigOverrideMap) { + final FormatterCallback formatterCallback = new FormatterCallback(); + + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV3.class, RuleSetProviderV3.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); + + EditorConfigOverride editorConfigOverride; + if (editorConfigOverrideMap.isEmpty()) { + editorConfigOverride = EditorConfigOverride.Companion.getEMPTY_EDITOR_CONFIG_OVERRIDE(); + } else { + editorConfigOverride = createEditorConfigOverride(allRuleProviders.stream().map( + RuleProvider::createNewRuleInstance).collect(Collectors.toList()), + editorConfigOverrideMap); + } + EditorConfigDefaults editorConfig; + if (editorConfigPath == null || !Files.exists(editorConfigPath)) { + editorConfig = EditorConfigDefaults.Companion.getEMPTY_EDITOR_CONFIG_DEFAULTS(); + } else { + editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath, RuleProviderKt.propertyTypes(allRuleProviders)); + } + + // create Code and then set the content to match previous steps in the Spotless pipeline + Code code = Code.Companion.fromPath(path); + KtLintCompatAdapter.setCodeContent(code, unix); + return new KtLintRuleEngine( + allRuleProviders, + editorConfig, + editorConfigOverride, + false, + path.getFileSystem()) + .format(code, formatterCallback); + } + + /** + * Create EditorConfigOverride from user provided parameters. + */ + private static EditorConfigOverride createEditorConfigOverride(final List rules, Map editorConfigOverrideMap) { + // Get properties from rules in the rule sets + Stream> ruleProperties = rules.stream() + .flatMap(rule -> rule.getUsesEditorConfigProperties().stream()); + + // Create a mapping of properties to their names based on rule properties and default properties + Map> supportedProperties = Stream + .concat(ruleProperties, DEFAULT_EDITOR_CONFIG_PROPERTIES.stream()) + .distinct() + .collect(Collectors.toMap(EditorConfigProperty::getName, property -> property)); + + // Create config properties based on provided property names and values + @SuppressWarnings("unchecked") + Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() + .map(entry -> { + EditorConfigProperty property = supportedProperties.get(entry.getKey()); + + if (property == null && entry.getKey().startsWith("ktlint_")) { + String[] parts = entry.getKey().substring(7).split("_", 2); + if (parts.length == 1) { + // convert ktlint_{ruleset} to {ruleset} + String id = parts[0]; + property = createRuleSetExecution(id, RuleExecution.enabled); + } else { + // convert ktlint_{ruleset}_{rulename} to {ruleset}:{rulename} + String id = parts[0] + ":" + parts[1]; + property = createRuleExecution(id, RuleExecution.enabled); + } + } + + if (property == null) { + return null; + } else { + return new Pair<>(property, entry.getValue()); + } + }) + .filter(Objects::nonNull) + .toArray(Pair[]::new); + + return EditorConfigOverride.Companion.from(properties); + } +} diff --git a/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java b/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java new file mode 100644 index 0000000000..951b82704a --- /dev/null +++ b/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java @@ -0,0 +1,169 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3; +import com.pinterest.ktlint.rule.engine.api.Code; +import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults; +import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride; +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine; +import com.pinterest.ktlint.rule.engine.api.LintError; +import com.pinterest.ktlint.rule.engine.core.api.Rule; +import com.pinterest.ktlint.rule.engine.core.api.RuleId; +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider; +import com.pinterest.ktlint.rule.engine.core.api.RuleProviderKt; +import com.pinterest.ktlint.rule.engine.core.api.RuleSetId; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EndOfLinePropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentSizeEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt; + +import kotlin.Pair; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + +public class KtLintCompat0Dot50Dot0Adapter implements KtLintCompatAdapter { + + private static final Logger logger = LoggerFactory.getLogger(KtLintCompat0Dot50Dot0Adapter.class); + + private static final List> DEFAULT_EDITOR_CONFIG_PROPERTIES; + + static { + DEFAULT_EDITOR_CONFIG_PROPERTIES = List.of( + CodeStyleEditorConfigPropertyKt.getCODE_STYLE_PROPERTY(), + EndOfLinePropertyKt.getEND_OF_LINE_PROPERTY(), + IndentSizeEditorConfigPropertyKt.getINDENT_SIZE_PROPERTY(), + IndentStyleEditorConfigPropertyKt.getINDENT_STYLE_PROPERTY(), + InsertFinalNewLineEditorConfigPropertyKt.getINSERT_FINAL_NEWLINE_PROPERTY(), + MaxLineLengthEditorConfigPropertyKt.getMAX_LINE_LENGTH_PROPERTY(), + RuleExecutionEditorConfigPropertyKt.getEXPERIMENTAL_RULES_EXECUTION_PROPERTY()); + } + + static class FormatterCallback implements Function2 { + + @Override + public Unit invoke(LintError lint, Boolean corrected) { + if (!corrected) { + KtLintCompatReporting.report(lint.getLine(), lint.getCol(), lint.getRuleId().getValue(), lint.getDetail()); + } + return Unit.INSTANCE; + } + } + + @Override + public String format( + String unix, + Path path, + Path editorConfigPath, + Map editorConfigOverrideMap) { + final FormatterCallback formatterCallback = new FormatterCallback(); + + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV3.class, RuleSetProviderV3.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); + + EditorConfigOverride editorConfigOverride; + if (editorConfigOverrideMap.isEmpty()) { + editorConfigOverride = EditorConfigOverride.Companion.getEMPTY_EDITOR_CONFIG_OVERRIDE(); + } else { + editorConfigOverride = createEditorConfigOverride(allRuleProviders.stream().map( + RuleProvider::createNewRuleInstance).collect(Collectors.toList()), + editorConfigOverrideMap); + } + EditorConfigDefaults editorConfig; + if (editorConfigPath == null || !Files.exists(editorConfigPath)) { + editorConfig = EditorConfigDefaults.Companion.getEMPTY_EDITOR_CONFIG_DEFAULTS(); + } else { + editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath, RuleProviderKt.propertyTypes(allRuleProviders)); + } + + // create Code and then set the content to match previous steps in the Spotless pipeline + Code code = Code.Companion.fromPath(path); + KtLintCompatAdapter.setCodeContent(code, unix); + return new KtLintRuleEngine( + allRuleProviders, + editorConfig, + editorConfigOverride, + true, + false, + path.getFileSystem()) + .format(code, formatterCallback); + } + + /** + * Create EditorConfigOverride from user provided parameters. + */ + private static EditorConfigOverride createEditorConfigOverride(final List rules, Map editorConfigOverrideMap) { + // Get properties from rules in the rule sets + Stream> ruleProperties = rules.stream() + .flatMap(rule -> rule.getUsesEditorConfigProperties().stream()); + + // Create a mapping of properties to their names based on rule properties and default properties + Map> supportedProperties = Stream + .concat(ruleProperties, DEFAULT_EDITOR_CONFIG_PROPERTIES.stream()) + .distinct() + .collect(Collectors.toMap(EditorConfigProperty::getName, property -> property)); + + // Create config properties based on provided property names and values + @SuppressWarnings("unchecked") + Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() + .map(entry -> { + EditorConfigProperty property = supportedProperties.get(entry.getKey()); + + if (property == null && entry.getKey().startsWith("ktlint_")) { + String[] parts = entry.getKey().substring(7).split("_", 2); + if (parts.length == 1) { + // convert ktlint_{ruleset} to RuleSetId + RuleSetId id = new RuleSetId(parts[0]); + property = RuleExecutionEditorConfigPropertyKt.createRuleSetExecutionEditorConfigProperty(id, RuleExecution.enabled); + } else { + // convert ktlint_{ruleset}_{rulename} to RuleId + RuleId id = new RuleId(parts[0] + ":" + parts[1]); + property = RuleExecutionEditorConfigPropertyKt.createRuleExecutionEditorConfigProperty(id, RuleExecution.enabled); + } + } + + if (property == null) { + return null; + } else { + return new Pair<>(property, entry.getValue()); + } + }) + .filter(Objects::nonNull) + .toArray(Pair[]::new); + + return EditorConfigOverride.Companion.from(properties); + } +} diff --git a/lib/src/compatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0Adapter.java b/lib/src/compatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0Adapter.java new file mode 100644 index 0000000000..86387d0d6b --- /dev/null +++ b/lib/src/compatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0Adapter.java @@ -0,0 +1,170 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3; +import com.pinterest.ktlint.rule.engine.api.Code; +import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults; +import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride; +import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine; +import com.pinterest.ktlint.rule.engine.api.LintError; +import com.pinterest.ktlint.rule.engine.core.api.Rule; +import com.pinterest.ktlint.rule.engine.core.api.RuleId; +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider; +import com.pinterest.ktlint.rule.engine.core.api.RuleProviderKt; +import com.pinterest.ktlint.rule.engine.core.api.RuleSetId; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EndOfLinePropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentSizeEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentStyleEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution; +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt; + +import kotlin.Pair; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; + +public class KtLintCompat1Dot0Dot0Adapter implements KtLintCompatAdapter { + + private static final Logger logger = LoggerFactory.getLogger(KtLintCompat1Dot0Dot0Adapter.class); + + private static final List> DEFAULT_EDITOR_CONFIG_PROPERTIES; + + static { + DEFAULT_EDITOR_CONFIG_PROPERTIES = List.of( + CodeStyleEditorConfigPropertyKt.getCODE_STYLE_PROPERTY(), + EndOfLinePropertyKt.getEND_OF_LINE_PROPERTY(), + IndentSizeEditorConfigPropertyKt.getINDENT_SIZE_PROPERTY(), + IndentStyleEditorConfigPropertyKt.getINDENT_STYLE_PROPERTY(), + InsertFinalNewLineEditorConfigPropertyKt.getINSERT_FINAL_NEWLINE_PROPERTY(), + MaxLineLengthEditorConfigPropertyKt.getMAX_LINE_LENGTH_PROPERTY(), + RuleExecutionEditorConfigPropertyKt.getEXPERIMENTAL_RULES_EXECUTION_PROPERTY()); + } + + static class FormatterCallback implements Function2 { + + @Override + public Unit invoke(LintError lint, Boolean corrected) { + if (!corrected) { + KtLintCompatReporting.report(lint.getLine(), lint.getCol(), lint.getRuleId().getValue(), lint.getDetail()); + } + return Unit.INSTANCE; + } + } + + @Override + public String format( + String unix, + Path path, + Path editorConfigPath, + Map editorConfigOverrideMap) { + final FormatterCallback formatterCallback = new FormatterCallback(); + + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV3.class, RuleSetProviderV3.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); + + EditorConfigDefaults editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath, RuleProviderKt.propertyTypes(allRuleProviders)); + EditorConfigOverride editorConfigOverride; + if (editorConfigOverrideMap.isEmpty()) { + editorConfigOverride = EditorConfigOverride.Companion.getEMPTY_EDITOR_CONFIG_OVERRIDE(); + } else { + editorConfigOverride = createEditorConfigOverride( + editorConfig, + allRuleProviders.stream().map(RuleProvider::createNewRuleInstance).collect(Collectors.toList()), + editorConfigOverrideMap); + } + + // create Code and then set the content to match previous steps in the Spotless pipeline + Code code = Code.Companion.fromPath(path); + KtLintCompatAdapter.setCodeContent(code, unix); + return new KtLintRuleEngine( + allRuleProviders, + editorConfig, + editorConfigOverride, + false, + path.getFileSystem()) + .format(code, formatterCallback); + } + + /** + * Create EditorConfigOverride from user provided parameters. + */ + private static EditorConfigOverride createEditorConfigOverride(final EditorConfigDefaults editorConfig, final List rules, Map editorConfigOverrideMap) { + // Get properties from rules in the rule sets + Stream> ruleProperties = rules.stream() + .flatMap(rule -> rule.getUsesEditorConfigProperties().stream()); + + // Create a mapping of properties to their names based on rule properties and default properties + Map> supportedProperties = Stream + .concat(ruleProperties, DEFAULT_EDITOR_CONFIG_PROPERTIES.stream()) + .distinct() + .collect(Collectors.toMap(EditorConfigProperty::getName, property -> property)); + + // The default style had been changed from intellij_idea to ktlint_official in version 1.0.0 + boolean isCodeStyleDefinedInEditorConfig = editorConfig.getValue().getSections().stream() + .anyMatch(section -> section.getProperties().containsKey("ktlint_code_style")); + if (!isCodeStyleDefinedInEditorConfig && !editorConfigOverrideMap.containsKey("ktlint_code_style")) { + editorConfigOverrideMap.put("ktlint_code_style", "intellij_idea"); + } + + // Create config properties based on provided property names and values + @SuppressWarnings("unchecked") + Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() + .map(entry -> { + EditorConfigProperty property = supportedProperties.get(entry.getKey()); + + if (property == null && entry.getKey().startsWith("ktlint_")) { + String[] parts = entry.getKey().substring(7).split("_", 2); + if (parts.length == 1) { + // convert ktlint_{ruleset} to RuleSetId + RuleSetId id = new RuleSetId(parts[0]); + property = RuleExecutionEditorConfigPropertyKt.createRuleSetExecutionEditorConfigProperty(id, RuleExecution.enabled); + } else { + // convert ktlint_{ruleset}_{rulename} to RuleId + RuleId id = new RuleId(parts[0] + ":" + parts[1]); + property = RuleExecutionEditorConfigPropertyKt.createRuleExecutionEditorConfigProperty(id, RuleExecution.enabled); + } + } + + if (property == null) { + return null; + } else { + return new Pair<>(property, entry.getValue()); + } + }) + .filter(Objects::nonNull) + .toArray(Pair[]::new); + + return EditorConfigOverride.Companion.from(properties); + } +} diff --git a/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java new file mode 100644 index 0000000000..0fcecb3f6c --- /dev/null +++ b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022-2024 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.ktlint.compat; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Map; + +public interface KtLintCompatAdapter { + + String format( + String content, + Path path, + Path editorConfigPath, + Map editorConfigOverrideMap) throws NoSuchFieldException, IllegalAccessException; + + static void setCodeContent(Object code, String content) { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + Field contentField = code.getClass().getDeclaredField("content"); + contentField.setAccessible(true); + contentField.set(code, content); + } catch (NoSuchFieldException | IllegalAccessException e) { + // Handle exceptions as needed + throw new RuntimeException("Failed to set content field", e); + } + return null; + }); + } +} diff --git a/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatReporting.java b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatReporting.java new file mode 100644 index 0000000000..5b6fcc70bf --- /dev/null +++ b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatReporting.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022-2024 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.ktlint.compat; + +public final class KtLintCompatReporting { + + private KtLintCompatReporting() {} + + static void report(int line, int column, String ruleId, String detail) { + throw new KtlintSpotlessException(line, ruleId, detail); + } + + public static class KtlintSpotlessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public final int line; + public final String ruleId; + public final String detail; + + KtlintSpotlessException(int line, String ruleId, String detail) { + this.line = line; + this.ruleId = ruleId; + this.detail = detail; + } + } +} diff --git a/lib/src/diktat/java/com/diffplug/spotless/glue/diktat/DiktatFormatterFunc.java b/lib/src/diktat/java/com/diffplug/spotless/glue/diktat/DiktatFormatterFunc.java new file mode 100644 index 0000000000..700c84da45 --- /dev/null +++ b/lib/src/diktat/java/com/diffplug/spotless/glue/diktat/DiktatFormatterFunc.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021-2024 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.diktat; + +import java.io.File; +import java.util.stream.Collectors; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.Lint; +import com.diffplug.spotless.glue.diktat.compat.DiktatCompat1Dot2Dot5Adapter; +import com.diffplug.spotless.glue.diktat.compat.DiktatCompat2Dot0Dot0Adapter; +import com.diffplug.spotless.glue.diktat.compat.DiktatCompatAdapter; +import com.diffplug.spotless.glue.diktat.compat.DiktatReporting; + +public class DiktatFormatterFunc implements FormatterFunc.NeedsFile { + private final DiktatCompatAdapter adapter; + private final boolean isScript; + + public DiktatFormatterFunc( + String version, + File configFile, + boolean isScript) { + if (version.startsWith("1.")) { + this.adapter = new DiktatCompat1Dot2Dot5Adapter(configFile); + } else { + this.adapter = new DiktatCompat2Dot0Dot0Adapter(configFile); + } + this.isScript = isScript; + } + + @Override + public String applyWithFile(String unix, File file) { + try { + return adapter.format(file, unix, isScript); + } catch (DiktatReporting.LintException e) { + throw Lint.shortcut(e.lints.stream().map(lint -> Lint.atLine(lint.line, lint.ruleId, lint.detail)).collect(Collectors.toList())); + } + } +} diff --git a/lib/src/gherkin/java/com/diffplug/spotless/glue/gherkin/GherkinUtilsFormatterFunc.java b/lib/src/gherkin/java/com/diffplug/spotless/glue/gherkin/GherkinUtilsFormatterFunc.java new file mode 100644 index 0000000000..4b79347b29 --- /dev/null +++ b/lib/src/gherkin/java/com/diffplug/spotless/glue/gherkin/GherkinUtilsFormatterFunc.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 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.gherkin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.gherkin.GherkinUtilsConfig; + +import io.cucumber.gherkin.GherkinParser; +import io.cucumber.gherkin.utils.pretty.Pretty; +import io.cucumber.gherkin.utils.pretty.Syntax; +import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.GherkinDocument; +import io.cucumber.messages.types.Source; +import io.cucumber.messages.types.SourceMediaType; + +public class GherkinUtilsFormatterFunc implements FormatterFunc { + private static final Logger LOGGER = LoggerFactory.getLogger(GherkinUtilsFormatterFunc.class); + + private final GherkinUtilsConfig gherkinSimpleConfig; + + public GherkinUtilsFormatterFunc(GherkinUtilsConfig gherkinSimpleConfig) { + this.gherkinSimpleConfig = gherkinSimpleConfig; + } + + // Follows https://github.com/cucumber/gherkin-utils/blob/main/java/src/test/java/io/cucumber/gherkin/utils/pretty/PrettyTest.java + private GherkinDocument parse(String gherkin) { + GherkinParser parser = GherkinParser + .builder() + .includeSource(false) + .build(); + return parser.parse(Envelope.of(new Source("test.feature", gherkin, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No envelope")) + .getGherkinDocument() + .orElseThrow(() -> new IllegalArgumentException("No gherkin document")); + } + + @Override + public String apply(String inputString) { + GherkinDocument gherkinDocument = parse(inputString); + + return Pretty.prettyPrint(gherkinDocument, Syntax.gherkin); + } +} diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java new file mode 100644 index 0000000000..d13f8d9d40 --- /dev/null +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 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.java; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +import com.google.googlejavaformat.java.Formatter; +import com.google.googlejavaformat.java.FormatterException; +import com.google.googlejavaformat.java.ImportOrderer; +import com.google.googlejavaformat.java.JavaFormatterOptions; +import com.google.googlejavaformat.java.JavaFormatterOptions.Style; +import com.google.googlejavaformat.java.RemoveUnusedImports; +import com.google.googlejavaformat.java.StringWrapper; + +import com.diffplug.spotless.FormatterFunc; + +// Used via reflection by the Gradle plugin. +@SuppressWarnings("unused") +public class GoogleJavaFormatFormatterFunc implements FormatterFunc { + + @Nonnull + private final Formatter formatter; + + @Nonnull + private final String version; + + @Nonnull + private final Style formatterStyle; + + private final boolean reflowStrings; + + private final boolean reorderImports; + + public GoogleJavaFormatFormatterFunc(@Nonnull String version, @Nonnull String style, boolean reflowStrings, boolean reorderImports, boolean formatJavadoc) { + this.version = Objects.requireNonNull(version); + this.formatterStyle = Style.valueOf(Objects.requireNonNull(style)); + this.reflowStrings = reflowStrings; + this.reorderImports = reorderImports; + + JavaFormatterOptions.Builder builder = JavaFormatterOptions.builder().style(formatterStyle); + if (!formatJavadoc) { + builder = builder.formatJavadoc(false); + } + this.formatter = new Formatter(builder.build()); + } + + @Override + @Nonnull + public String apply(@Nonnull String input) throws Exception { + String formatted = formatter.formatSource(input); + String removedUnused = RemoveUnusedImports.removeUnusedImports(formatted); + String sortedImports = ImportOrderer.reorderImports(removedUnused, reorderImports ? formatterStyle : Style.GOOGLE); + return reflowLongStrings(sortedImports); + } + + private String reflowLongStrings(String input) throws FormatterException { + if (reflowStrings) { + return StringWrapper.wrap(input, formatter); + } else { + return input; + } + } +} diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java new file mode 100644 index 0000000000..de21a18795 --- /dev/null +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatRemoveUnusedImporterFormatterFunc.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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.java; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +import com.google.googlejavaformat.java.RemoveUnusedImports; + +import com.diffplug.spotless.FormatterFunc; + +public class GoogleJavaFormatRemoveUnusedImporterFormatterFunc implements FormatterFunc { + + @Nonnull + private final String version; + + public GoogleJavaFormatRemoveUnusedImporterFormatterFunc(@Nonnull String version) { + this.version = Objects.requireNonNull(version); + } + + @Override + @Nonnull + public String apply(@Nonnull String input) throws Exception { + return RemoveUnusedImports.removeUnusedImports(input); + } +} diff --git a/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java b/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java new file mode 100644 index 0000000000..d1533d6b43 --- /dev/null +++ b/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023-2024 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.gson; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonWriter; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.json.gson.GsonConfig; + +public class GsonFormatterFunc implements FormatterFunc { + + private static final String FAILED_TO_PARSE_ERROR_MESSAGE = "Unable to parse JSON"; + + private final Gson gson; + private final GsonConfig gsonConfig; + private final String generatedIndent; + + public GsonFormatterFunc(GsonConfig gsonConfig) { + GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls(); + if (!gsonConfig.isEscapeHtml()) { + gsonBuilder = gsonBuilder.disableHtmlEscaping(); + } + this.gson = gsonBuilder.create(); + this.gsonConfig = gsonConfig; + this.generatedIndent = generateIndent(gsonConfig.getIndentSpaces()); + } + + @Override + public String apply(String inputString) { + String result; + if (inputString.isEmpty()) { + result = ""; + } else { + JsonElement jsonElement = gson.fromJson(inputString, JsonElement.class); + if (jsonElement == null) { + throw new IllegalArgumentException(FAILED_TO_PARSE_ERROR_MESSAGE); + } + if (gsonConfig.isSortByKeys()) { + jsonElement = sortByKeys(jsonElement); + } + try (StringWriter stringWriter = new StringWriter()) { + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(this.generatedIndent); + gson.toJson(jsonElement, jsonWriter); + result = stringWriter + "\n"; + } catch (IOException ioException) { + throw ThrowingEx.asRuntime(ioException); + } + } + return result; + } + + private JsonElement sortByKeys(JsonElement jsonElement) { + if (jsonElement.isJsonArray()) { + return sortByKeys(jsonElement.getAsJsonArray()); + } else if (jsonElement.isJsonObject()) { + return sortByKeys(jsonElement.getAsJsonObject()); + } else { + return jsonElement; + } + } + + private JsonElement sortByKeys(JsonObject jsonObject) { + JsonObject result = new JsonObject(); + jsonObject.keySet().stream().sorted() + .forEach(key -> { + JsonElement sorted = sortByKeys(jsonObject.get(key)); + result.add(key, sorted); + }); + return result; + } + + private JsonElement sortByKeys(JsonArray jsonArray) { + var result = new JsonArray(); + for (JsonElement element : jsonArray) { + JsonElement sorted = sortByKeys(element); + result.add(sorted); + } + + return result; + } + + private String generateIndent(int indentSpaces) { + return String.join("", Collections.nCopies(indentSpaces, " ")); + } + +} diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java new file mode 100644 index 0000000000..1f317fa3b4 --- /dev/null +++ b/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021-2023 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.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.json.JacksonConfig; + +/** + * A {@link FormatterFunc} based on Jackson library + */ +// https://github.com/FasterXML/jackson-dataformats-text/issues/372 +public abstract class AJacksonFormatterFunc implements FormatterFunc { + private JacksonConfig jacksonConfig; + + public AJacksonFormatterFunc(JacksonConfig jacksonConfig) { + this.jacksonConfig = jacksonConfig; + } + + @Override + public String apply(String input) throws Exception { + ObjectMapper objectMapper = makeObjectMapper(); + + return format(objectMapper, input); + } + + protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException { + try { + // ObjectNode is not compatible with SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS + Object objectNode = objectMapper.readValue(input, inferType(input)); + String output = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectNode); + + return output; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to format. input='" + input + "'", e); + } + } + + /** + * + * @param input + * @return the {@link Class} into which the String has to be deserialized + */ + protected abstract Class inferType(String input); + + /** + * @return a {@link JsonFactory}. May be overridden to handle alternative formats. + * @see jackson-dataformats-text + */ + protected abstract JsonFactory makeJsonFactory(); + + protected ObjectMapper makeObjectMapper() { + JsonFactory jsonFactory = makeJsonFactory(); + ObjectMapper objectMapper = new ObjectMapper(jsonFactory); + + objectMapper.setDefaultPrettyPrinter(makePrettyPrinter()); + + // Configure the ObjectMapper + // https://github.com/FasterXML/jackson-databind#commonly-used-features + jacksonConfig.getFeatureToToggle().forEach((rawFeature, toggle) -> { + // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection + SerializationFeature feature = SerializationFeature.valueOf(rawFeature); + + objectMapper.configure(feature, toggle); + }); + + return objectMapper; + } + + protected PrettyPrinter makePrettyPrinter() { + return new DefaultPrettyPrinter(); + } +} diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java new file mode 100644 index 0000000000..71215eb4a3 --- /dev/null +++ b/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021-2023 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.json; + +import java.util.Collection; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonFactoryBuilder; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.Separators; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.json.JacksonJsonConfig; + +/** + * A {@link FormatterFunc} based on Jackson library + */ +// https://github.com/FasterXML/jackson-dataformats-text/issues/372 +public class JacksonJsonFormatterFunc extends AJacksonFormatterFunc { + private JacksonJsonConfig jacksonConfig; + + public JacksonJsonFormatterFunc(JacksonJsonConfig jacksonConfig) { + super(jacksonConfig); + this.jacksonConfig = jacksonConfig; + } + + @Override + protected Class inferType(String input) { + if (input.trim().startsWith("[")) { + return Collection.class; + } else { + return Map.class; + } + } + + /** + * @return a {@link JsonFactory}. May be overridden to handle alternative formats. + * @see jackson-dataformats-text + */ + protected JsonFactory makeJsonFactory() { + JsonFactory jsonFactory = new JsonFactoryBuilder().build(); + + // Configure the ObjectMapper + // https://github.com/FasterXML/jackson-databind#commonly-used-features + jacksonConfig.getJsonFeatureToToggle().forEach((rawFeature, toggle) -> { + // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection + JsonGenerator.Feature feature = JsonGenerator.Feature.valueOf(rawFeature); + + jsonFactory.configure(feature, toggle); + }); + + return jsonFactory; + } + + @Override + protected DefaultPrettyPrinter makePrettyPrinter() { + boolean spaceBeforeSeparator = jacksonConfig.isSpaceBeforeSeparator(); + + // DefaultIndenter default constructor relies on 2 whitespaces as default tabulation + // By we want to force '\n' as eol given Spotless provides LF-input (whatever the actual File content/current OS) + DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(" ", "\n"); + DefaultPrettyPrinter printer = new SpotlessJsonPrettyPrinter(spaceBeforeSeparator); + + printer.indentObjectsWith(indenter); + printer.indentArraysWith(indenter); + return printer; + } + + protected static class SpotlessJsonPrettyPrinter extends DefaultPrettyPrinter { + private static final long serialVersionUID = 1L; + private final boolean spaceBeforeSeparator; + + public SpotlessJsonPrettyPrinter(boolean spaceBeforeSeparator) { + this.spaceBeforeSeparator = spaceBeforeSeparator; + } + + @Override + public DefaultPrettyPrinter createInstance() { + return new SpotlessJsonPrettyPrinter(spaceBeforeSeparator); + } + + @Override + public DefaultPrettyPrinter withSeparators(Separators separators) { + this._separators = separators; + if (spaceBeforeSeparator) { + // This is Jackson default behavior + this._objectFieldValueSeparatorWithSpaces = " " + separators.getObjectFieldValueSeparator() + " "; + } else { + this._objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " "; + } + return this; + } + } +} diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java new file mode 100644 index 0000000000..b4f8ca6aee --- /dev/null +++ b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021-2023 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.yaml; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; + +import com.diffplug.spotless.glue.json.AJacksonFormatterFunc; +import com.diffplug.spotless.yaml.JacksonYamlConfig; + +public class JacksonYamlFormatterFunc extends AJacksonFormatterFunc { + final JacksonYamlConfig yamlConfig; + + public JacksonYamlFormatterFunc(JacksonYamlConfig jacksonConfig) { + super(jacksonConfig); + this.yamlConfig = jacksonConfig; + + if (jacksonConfig == null) { + throw new IllegalArgumentException("ARG"); + } + } + + protected JsonFactory makeJsonFactory() { + YAMLFactoryBuilder yamlFactoryBuilder = new YAMLFactoryBuilder(new YAMLFactory()); + + // Configure the ObjectMapper + // https://github.com/FasterXML/jackson-databind#commonly-used-features + yamlConfig.getYamlFeatureToToggle().forEach((rawFeature, toggle) -> { + // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection + YAMLGenerator.Feature feature = YAMLGenerator.Feature.valueOf(rawFeature); + + yamlFactoryBuilder.configure(feature, toggle); + }); + + return yamlFactoryBuilder.build(); + } + + @Override + protected Class inferType(String input) { + return JsonNode.class; + } + + @Override + protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException { + try { + // https://stackoverflow.com/questions/25222327/deserialize-pojos-from-multiple-yaml-documents-in-a-single-file-in-jackson + // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-375328648 + JsonParser yamlParser = objectMapper.getFactory().createParser(input); + List documents = objectMapper.readValues(yamlParser, inferType(input)).readAll(); + + // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-554265055 + // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-554265055 + StringWriter stringWriter = new StringWriter(); + objectMapper.writer().writeValues(stringWriter).writeAll(documents).close(); + return stringWriter.toString(); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to format. input='" + input + "'", e); + } + } +} 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 new file mode 100644 index 0000000000..bb4c9050bd --- /dev/null +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022-2024 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.ktfmt; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.facebook.ktfmt.format.Formatter; +import com.facebook.ktfmt.format.FormattingOptions; + +import com.diffplug.spotless.FormatterFunc; + +public final class KtfmtFormatterFunc implements FormatterFunc { + + @Nonnull + private final KtfmtStyle style; + + @Nullable + private final KtfmtFormattingOptions ktfmtFormattingOptions; + + public KtfmtFormatterFunc() { + this(KtfmtStyle.META, null); + } + + public KtfmtFormatterFunc(@Nonnull KtfmtStyle style) { + this(style, null); + } + + public KtfmtFormatterFunc(@Nullable KtfmtFormattingOptions ktfmtFormattingOptions) { + this(KtfmtStyle.META, ktfmtFormattingOptions); + } + + public KtfmtFormatterFunc(@Nonnull KtfmtStyle style, @Nullable KtfmtFormattingOptions ktfmtFormattingOptions) { + this.style = style; + this.ktfmtFormattingOptions = ktfmtFormattingOptions; + } + + @Nonnull + @Override + public String apply(@Nonnull String input) throws Exception { + return Formatter.format(createFormattingOptions(), input); + } + + private FormattingOptions createFormattingOptions() throws Exception { + FormattingOptions formattingOptions; + switch (style) { + case META: + formattingOptions = Formatter.META_FORMAT; + break; + case GOOGLE: + formattingOptions = Formatter.GOOGLE_FORMAT; + break; + case KOTLIN_LANG: + formattingOptions = Formatter.KOTLINLANG_FORMAT; + break; + default: + throw new IllegalStateException("Unknown formatting option " + style); + } + + if (ktfmtFormattingOptions != null) { + formattingOptions = formattingOptions.copy( + ktfmtFormattingOptions.getMaxWidth().orElse(formattingOptions.getMaxWidth()), + ktfmtFormattingOptions.getBlockIndent().orElse(formattingOptions.getBlockIndent()), + ktfmtFormattingOptions.getContinuationIndent().orElse(formattingOptions.getContinuationIndent()), + ktfmtFormattingOptions.getManageTrailingCommas().orElse(formattingOptions.getManageTrailingCommas()), + ktfmtFormattingOptions.getRemoveUnusedImports().orElse(formattingOptions.getRemoveUnusedImports()), + formattingOptions.getDebuggingPrintOpsAfterFormatting()); + } + + return formattingOptions; + } +} diff --git a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormattingOptions.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormattingOptions.java new file mode 100644 index 0000000000..0a810152b6 --- /dev/null +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormattingOptions.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022-2024 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.ktfmt; + +import java.util.Optional; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class KtfmtFormattingOptions { + + @Nullable + private Integer maxWidth; + + @Nullable + private Integer blockIndent; + + @Nullable + private Integer continuationIndent; + + @Nullable + private Boolean removeUnusedImports; + + @Nullable + private Boolean manageTrailingCommas; + + public KtfmtFormattingOptions( + @Nullable Integer maxWidth, + @Nullable Integer blockIndent, + @Nullable Integer continuationIndent, + @Nullable Boolean removeUnusedImports, + @Nullable Boolean manageTrailingCommas) { + this.maxWidth = maxWidth; + this.blockIndent = blockIndent; + this.continuationIndent = continuationIndent; + this.removeUnusedImports = removeUnusedImports; + this.manageTrailingCommas = manageTrailingCommas; + } + + @Nonnull + public Optional getMaxWidth() { + return Optional.ofNullable(maxWidth); + } + + @Nonnull + public Optional getBlockIndent() { + return Optional.ofNullable(blockIndent); + } + + @Nonnull + public Optional getContinuationIndent() { + return Optional.ofNullable(continuationIndent); + } + + @Nonnull + public Optional getRemoveUnusedImports() { + return Optional.ofNullable(removeUnusedImports); + } + + @Nonnull + public Optional getManageTrailingCommas() { + return Optional.ofNullable(manageTrailingCommas); + } + + public void setMaxWidth(int maxWidth) { + if (maxWidth <= 0) { + throw new IllegalArgumentException("Max width cannot be negative value or 0"); + } + this.maxWidth = maxWidth; + } + + public void setBlockIndent(int blockIndent) { + if (blockIndent < 0) { + throw new IllegalArgumentException("Block indent cannot be negative value"); + } + this.blockIndent = blockIndent; + } + + public void setContinuationIndent(int continuationIndent) { + if (continuationIndent < 0) { + throw new IllegalArgumentException("Continuation indent cannot be negative value"); + } + this.continuationIndent = continuationIndent; + } + + public void setRemoveUnusedImports(boolean removeUnusedImports) { + this.removeUnusedImports = removeUnusedImports; + } + + public void setManageTrailingCommas(boolean manageTrailingCommas) { + this.manageTrailingCommas = manageTrailingCommas; + } +} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/package-info.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtStyle.java similarity index 81% rename from _ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/package-info.java rename to lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtStyle.java index b444c0c76a..47aea7eedd 100644 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/service/package-info.java +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtStyle.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2022-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Mocking Eclipse Equinox OSGi services. */ -package com.diffplug.spotless.extra.eclipse.base.service; +package com.diffplug.spotless.glue.ktfmt; + +public enum KtfmtStyle { + META, GOOGLE, KOTLIN_LANG +} diff --git a/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java b/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java index 2147c22e90..a572d3a577 100644 --- a/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java +++ b/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,57 +16,64 @@ package com.diffplug.spotless.glue.ktlint; import java.io.File; -import java.util.Collections; -import java.util.List; +import java.nio.file.Path; import java.util.Map; -import java.util.logging.Logger; - -import com.pinterest.ktlint.core.KtLint; -import com.pinterest.ktlint.core.KtLint.Params; -import com.pinterest.ktlint.core.LintError; -import com.pinterest.ktlint.core.RuleSet; -import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider; +import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; - -import kotlin.Unit; -import kotlin.jvm.functions.Function2; +import com.diffplug.spotless.Lint; +import com.diffplug.spotless.glue.ktlint.compat.*; public class KtlintFormatterFunc implements FormatterFunc.NeedsFile { - private static final Logger logger = Logger.getLogger(KtlintFormatterFunc.class.getName()); + private final KtLintCompatAdapter adapter; + private final FileSignature editorConfigPath; + private final Map editorConfigOverrideMap; - private final List rulesets; - private final Map userData; - private final Function2 formatterCallback; - private final boolean isScript; - - public KtlintFormatterFunc(boolean isScript, Map userData) { - rulesets = Collections.singletonList(new StandardRuleSetProvider().get()); - this.userData = userData; - formatterCallback = new FormatterCallback(); - this.isScript = isScript; - } - - static class FormatterCallback implements Function2 { - @Override - public Unit invoke(LintError lint, Boolean corrected) { - if (!corrected) { - throw new AssertionError("Error on line: " + lint.getLine() + ", column: " + lint.getCol() + "\n" + lint.getDetail()); + public KtlintFormatterFunc( + String version, + FileSignature editorConfigPath, + Map editorConfigOverrideMap) { + String[] versions = version.split("\\."); + int majorVersion = Integer.parseInt(versions[0]); + int minorVersion = Integer.parseInt(versions[1]); + if (majorVersion == 1) { + this.adapter = new KtLintCompat1Dot0Dot0Adapter(); + } else { + if (minorVersion >= 50) { + // Fixed `RuleId` and `RuleSetId` issues + // New argument to `EditorConfigDefaults.Companion.load(...)` for custom property type parsing + // New argument to `new KtLintRuleEngine(...)` to fail on usage of `treeCopyHandler` extension point + this.adapter = new KtLintCompat0Dot50Dot0Adapter(); + } else if (minorVersion == 49) { + // Packages and modules moved around (`ktlint-core` -> `ktlint-rule-engine`) + // Experimental ruleset was replaced by implementing `Rule.Experimental` and checking the `ktlint_experimental` `.editorconfig` property + // `RuleId` and `RuleSetId` became inline classes (mangled to be unrepresentable in Java source code, so reflection is needed), tracked here: https://github.com/pinterest/ktlint/issues/2041 + this.adapter = new KtLintCompat0Dot49Dot0Adapter(); + } else if (minorVersion == 48) { + // ExperimentalParams lost two constructor arguments, EditorConfigProperty moved to its own class + this.adapter = new KtLintCompat0Dot48Dot0Adapter(); + } else { + throw new IllegalStateException("Ktlint versions < 0.48.0 not supported!"); } - return null; } + this.editorConfigPath = editorConfigPath; + this.editorConfigOverrideMap = editorConfigOverrideMap; } @Override - public String applyWithFile(String unix, File file) throws Exception { - return KtLint.INSTANCE.format(new Params( - file.getName(), - unix, - rulesets, - userData, - formatterCallback, - isScript, - null, - false)); + public String applyWithFile(String unix, File file) throws NoSuchFieldException, IllegalAccessException { + Path absoluteEditorConfigPath = null; + if (editorConfigPath != null) { + absoluteEditorConfigPath = editorConfigPath.getOnlyFile().toPath(); + } + try { + return adapter.format( + unix, + file.toPath(), + absoluteEditorConfigPath, + editorConfigOverrideMap); + } catch (KtLintCompatReporting.KtlintSpotlessException e) { + throw Lint.atLine(e.line, e.ruleId, e.detail).shortcut(); + } } } diff --git a/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java b/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java new file mode 100644 index 0000000000..39be0d9559 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/ConfigurationCacheHackList.java @@ -0,0 +1,150 @@ +/* + * Copyright 2024-2025 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 java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import com.diffplug.spotless.yaml.SerializeToByteArrayHack; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Gradle requires three things: + * - Gradle defines cache equality based on your serialized representation + * - Combined with remote build cache, you cannot have any absolute paths in + * your serialized representation + * - Combined with configuration cache, you must be able to roundtrip yourself + * through serialization + * + * These requirements are at odds with each other, as described in these issues + * - Gradle issue to define custom equality + * https://github.com/gradle/gradle/issues/29816 + * - Spotless plea for developer cache instead of configuration cache + * https://github.com/diffplug/spotless/issues/987 + * - Spotless cache miss bug fixed by this class + * https://github.com/diffplug/spotless/issues/2168 + * + * This class is a `List` which can optimize the + * serialized representation for either + * - roundtrip integrity + * - OR + * - equality + * + * Because it is not possible to provide both at the same time. + * It is a horrific hack, but it works, and it's the only way I can figure + * to make Spotless work with all of Gradle's cache systems at once. + */ +public class ConfigurationCacheHackList implements java.io.Serializable { + private static final long serialVersionUID = 6914178791997323870L; + + private boolean optimizeForEquality; + private ArrayList backingList = new ArrayList<>(); + + private boolean shouldWeSerializeToByteArrayFirst() { + return backingList.stream().anyMatch(step -> step instanceof SerializeToByteArrayHack); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + boolean serializeToByteArrayFirst = shouldWeSerializeToByteArrayFirst(); + out.writeBoolean(serializeToByteArrayFirst); + out.writeBoolean(optimizeForEquality); + out.writeInt(backingList.size()); + for (Object obj : backingList) { + // if write out the list on its own, we'll get java's non-deterministic object-graph serialization + // by writing each object to raw bytes independently, we avoid this + if (serializeToByteArrayFirst) { + out.writeObject(LazyForwardingEquality.toBytes((Serializable) obj)); + } else { + out.writeObject(obj); + } + } + } + + @SuppressFBWarnings("MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT") + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + boolean serializeToByteArrayFirst = in.readBoolean(); + optimizeForEquality = in.readBoolean(); + backingList = new ArrayList<>(); + int size = in.readInt(); + for (int i = 0; i < size; i++) { + if (serializeToByteArrayFirst) { + backingList.add(LazyForwardingEquality.fromBytes((byte[]) in.readObject())); + } else { + backingList.add(in.readObject()); + } + } + } + + public static ConfigurationCacheHackList forEquality() { + return new ConfigurationCacheHackList(true); + } + + public static ConfigurationCacheHackList forRoundtrip() { + return new ConfigurationCacheHackList(false); + } + + private ConfigurationCacheHackList(boolean optimizeForEquality) { + this.optimizeForEquality = optimizeForEquality; + } + + public void clear() { + backingList.clear(); + } + + public void addAll(Collection c) { + for (FormatterStep step : c) { + if (step instanceof FormatterStepSerializationRoundtrip) { + var clone = ((FormatterStepSerializationRoundtrip) step).hackClone(optimizeForEquality); + backingList.add(clone); + } else { + backingList.add(step); + } + } + } + + public List getSteps() { + var result = new ArrayList(backingList.size()); + for (Object obj : backingList) { + if (obj instanceof FormatterStepSerializationRoundtrip.HackClone) { + result.add(((FormatterStepSerializationRoundtrip.HackClone) obj).rehydrate()); + } else { + result.add((FormatterStep) obj); + } + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ConfigurationCacheHackList stepList = (ConfigurationCacheHackList) o; + return optimizeForEquality == stepList.optimizeForEquality && + backingList.equals(stepList.backingList); + } + + @Override + public int hashCode() { + return Objects.hash(optimizeForEquality, backingList); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/DelegateFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/DelegateFormatterStep.java new file mode 100644 index 0000000000..18d64b4759 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/DelegateFormatterStep.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022-2024 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 java.util.Objects; + +/** Superclass of all compound FormatterSteps necessary for {@link com.diffplug.spotless.LazyForwardingEquality#unlazy(java.lang.Object)}. */ +abstract class DelegateFormatterStep implements FormatterStep { + protected final FormatterStep delegateStep; + + DelegateFormatterStep(FormatterStep delegateStep) { + this.delegateStep = Objects.requireNonNull(delegateStep); + } + + @Override + public final String getName() { + return delegateStep.getName(); + } + + @Override + public void close() throws Exception { + delegateStep.close(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/DirtyState.java b/lib/src/main/java/com/diffplug/spotless/DirtyState.java new file mode 100644 index 0000000000..a5d269aa70 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/DirtyState.java @@ -0,0 +1,130 @@ +/* + * Copyright 2022-2024 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 java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Arrays; + +import javax.annotation.Nullable; + +/** + * The clean/dirty state of a single file. Intended use: + * - {@link #isClean()} means that the file is is clean, and there's nothing else to say + * - {@link #didNotConverge()} means that we were unable to determine a clean state + * - once you've tested the above conditions and you know that it's a dirty file with a converged state, + * then you can call {@link #writeCanonicalTo(OutputStream)} to get the canonical form of the given file. + */ +public class DirtyState { + @Nullable + private final byte[] canonicalBytes; + + DirtyState(@Nullable byte[] canonicalBytes) { + this.canonicalBytes = canonicalBytes; + } + + public boolean isClean() { + return this == isClean; + } + + public boolean didNotConverge() { + return this == didNotConverge; + } + + byte[] canonicalBytes() { + if (canonicalBytes == null) { + throw new IllegalStateException("First make sure that {@code !isClean()} and {@code !didNotConverge()}"); + } + return canonicalBytes; + } + + public void writeCanonicalTo(File file) throws IOException { + Files.write(file.toPath(), canonicalBytes()); + } + + public void writeCanonicalTo(OutputStream out) throws IOException { + out.write(canonicalBytes()); + } + + /** Returns the DirtyState which corresponds to {@code isClean()}. */ + public static DirtyState clean() { + return isClean; + } + + static final DirtyState didNotConverge = new DirtyState(null); + static final DirtyState isClean = new DirtyState(null); + + public static DirtyState of(Formatter formatter, File file) throws IOException { + return of(formatter, file, Files.readAllBytes(file.toPath())); + } + + public static DirtyState of(Formatter formatter, File file, byte[] rawBytes) { + return of(formatter, file, rawBytes, new String(rawBytes, formatter.getEncoding())); + } + + public static DirtyState of(Formatter formatter, File file, byte[] rawBytes, String raw) { + var valuePerStep = new ValuePerStep(formatter); + DirtyState state = of(formatter, file, rawBytes, raw, valuePerStep); + Formatter.legacyErrorBehavior(formatter, file, valuePerStep); + return state; + } + + static DirtyState of(Formatter formatter, File file, byte[] rawBytes, String raw, ValuePerStep exceptionPerStep) { + // check that all characters were encodable + String encodingError = EncodingErrorMsg.msg(raw, rawBytes, formatter.getEncoding()); + if (encodingError != null) { + throw new IllegalArgumentException(encodingError); + } + + String rawUnix = LineEnding.toUnix(raw); + + // enforce the format + String formattedUnix = formatter.computeWithLint(rawUnix, file, exceptionPerStep); + // convert the line endings if necessary + String formatted = formatter.computeLineEndings(formattedUnix, file); + + // if F(input) == input, then the formatter is well-behaving and the input is clean + byte[] formattedBytes = formatted.getBytes(formatter.getEncoding()); + if (Arrays.equals(rawBytes, formattedBytes)) { + return isClean; + } + + // F(input) != input, so we'll do a padded check + String doubleFormattedUnix = formatter.computeWithLint(formattedUnix, file, exceptionPerStep); + if (doubleFormattedUnix.equals(formattedUnix)) { + // most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case + return new DirtyState(formattedBytes); + } + + PaddedCell cell = PaddedCell.check(formatter, file, rawUnix, exceptionPerStep); + if (!cell.isResolvable()) { + return didNotConverge; + } + + // get the canonical bytes + String canonicalUnix = cell.canonical(); + String canonical = formatter.computeLineEndings(canonicalUnix, file); + byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding()); + if (!Arrays.equals(rawBytes, canonicalBytes)) { + // and write them to disk if needed + return new DirtyState(canonicalBytes); + } else { + return isClean; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/EncodingErrorMsg.java b/lib/src/main/java/com/diffplug/spotless/EncodingErrorMsg.java index b89400b7c2..3e4bb3423b 100644 --- a/lib/src/main/java/com/diffplug/spotless/EncodingErrorMsg.java +++ b/lib/src/main/java/com/diffplug/spotless/EncodingErrorMsg.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java b/lib/src/main/java/com/diffplug/spotless/FeatureClassLoader.java index 506e29f49c..0eb5aa4d2e 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-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,19 +24,17 @@ import java.security.ProtectionDomain; import java.util.Objects; -import javax.annotation.Nullable; - /** * This class loader is used to load classes of Spotless features from a search * path of URLs.
* Features shall be independent from build tools. Hence the class loader of the - * underlying build tool is e.g. skipped during the the search for classes.
+ * underlying build tool is e.g. skipped during the search for classes.
* * For `com.diffplug.spotless.glue.`, classes are redefined from within the lib jar * but linked against the `Url[]`. This allows us to ship classfiles which function as glue * code but delay linking/definition to runtime after the user has specified which version * of the formatter they want. - * + *

* For `"org.slf4j.` and (`com.diffplug.spotless.` but not `com.diffplug.spotless.extra.`) * the classes are loaded from the buildToolClassLoader. */ @@ -64,7 +62,7 @@ class FeatureClassLoader extends URLClassLoader { @Override protected Class findClass(String name) throws ClassNotFoundException { - if (name.startsWith("com.diffplug.spotless.glue.")) { + if (name.startsWith("com.diffplug.spotless.glue.") || name.startsWith("com.diffplug.spotless.extra.glue.")) { String path = name.replace('.', '/') + ".class"; URL url = findResource(path); if (url == null) { @@ -103,35 +101,14 @@ public URL findResource(String name) { private static ByteBuffer urlToByteBuffer(URL url) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - InputStream inputStream = url.openStream(); - while ((nRead = inputStream.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); + try (InputStream inputStream = url.openStream()) { + inputStream.transferTo(buffer); } buffer.flush(); return ByteBuffer.wrap(buffer.toByteArray()); } - /** - * Making spotless Java 9+ compatible. In Java 8 (and minor) the bootstrap - * class loader saw every platform class. In Java 9+ it was changed so the - * bootstrap class loader does not see all classes anymore. This might lead - * to ClassNotFoundException in formatters (e.g. freshmark). - * - * @return null on Java 8 (and minor), otherwise PlatformClassLoader - */ - @Nullable private static ClassLoader getParentClassLoader() { - double version = Double.parseDouble(System.getProperty("java.specification.version")); - if (version > 1.8) { - try { - return (ClassLoader) ClassLoader.class.getMethod("getPlatformClassLoader").invoke(null); - } catch (Exception e) { - throw ThrowingEx.asRuntime(e); - } - } else { - return null; - } + return ThrowingEx.get(() -> (ClassLoader) ClassLoader.class.getMethod("getPlatformClassLoader").invoke(null)); } } diff --git a/lib/src/main/java/com/diffplug/spotless/FileSignature.java b/lib/src/main/java/com/diffplug/spotless/FileSignature.java index 59893f26e6..0d26d86ff8 100644 --- a/lib/src/main/java/com/diffplug/spotless/FileSignature.java +++ b/lib/src/main/java/com/diffplug/spotless/FileSignature.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.Map; +import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** Computes a signature for any needed files. */ @@ -43,6 +44,8 @@ public final class FileSignature implements Serializable { * Transient because not needed to uniquely identify a FileSignature instance, and also because * Gradle only needs this class to be Serializable so it can compare FileSignature instances for * incremental builds. + * + * We don't want these absolute paths to screw up buildcache keys. */ @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") private final transient List files; @@ -93,6 +96,39 @@ private FileSignature(final List files) throws IOException { } } + /** A view of `FileSignature` which can be safely roundtripped. */ + public static class Promised implements Serializable { + private static final long serialVersionUID = 1L; + private final List files; + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + private transient @Nullable FileSignature cached; + + private Promised(List files, @Nullable FileSignature cached) { + this.files = files; + this.cached = cached; + } + + public FileSignature get() { + if (cached == null) { + // null when restored via serialization + cached = ThrowingEx.get(() -> new FileSignature(files)); + } + return cached; + } + } + + public Promised asPromise() { + return new Promised(files, this); + } + + public static Promised promise(Iterable files) { + return new Promised(MoreIterables.toNullHostileList(files), null); + } + + public static Promised promise(File file) { + return new Promised(MoreIterables.toNullHostileList(List.of(file)), null); + } + /** Returns all of the files in this signature, throwing an exception if there are more or less than 1 file. */ public Collection files() { return Collections.unmodifiableList(files); diff --git a/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java index 9b39361719..bcc444ad57 100644 --- a/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,40 +16,44 @@ package com.diffplug.spotless; import java.io.File; +import java.util.List; import java.util.Objects; -import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; -final class FilterByContentPatternFormatterStep implements FormatterStep { - private final FormatterStep delegateStep; +final class FilterByContentPatternFormatterStep extends DelegateFormatterStep { + final OnMatch onMatch; final Pattern contentPattern; - FilterByContentPatternFormatterStep(FormatterStep delegateStep, String contentPattern) { - this.delegateStep = Objects.requireNonNull(delegateStep); + FilterByContentPatternFormatterStep(FormatterStep delegateStep, OnMatch onMatch, String contentPattern) { + super(delegateStep); + this.onMatch = onMatch; this.contentPattern = Pattern.compile(Objects.requireNonNull(contentPattern)); } - @Override - public String getName() { - return delegateStep.getName(); - } - @Override public @Nullable String format(String raw, File file) throws Exception { Objects.requireNonNull(raw, "raw"); Objects.requireNonNull(file, "file"); - - Matcher matcher = contentPattern.matcher(raw); - - if (matcher.find()) { + if (contentPattern.matcher(raw).find() == (onMatch == OnMatch.INCLUDE)) { return delegateStep.format(raw, file); } else { return raw; } } + @Override + public List lint(String raw, File file) throws Exception { + Objects.requireNonNull(raw, "raw"); + Objects.requireNonNull(file, "file"); + if (contentPattern.matcher(raw).find() == (onMatch == OnMatch.INCLUDE)) { + return delegateStep.lint(raw, file); + } else { + return List.of(); + } + } + @Override public boolean equals(Object o) { if (this == o) { @@ -60,13 +64,14 @@ public boolean equals(Object o) { } FilterByContentPatternFormatterStep that = (FilterByContentPatternFormatterStep) o; return Objects.equals(delegateStep, that.delegateStep) && + Objects.equals(onMatch, that.onMatch) && Objects.equals(contentPattern.pattern(), that.contentPattern.pattern()); } @Override public int hashCode() { - return Objects.hash(delegateStep, contentPattern.pattern()); + return Objects.hash(delegateStep, onMatch, contentPattern.pattern()); } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; } diff --git a/lib/src/main/java/com/diffplug/spotless/FilterByFileFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FilterByFileFormatterStep.java index 2fd221220c..bc5ddf6053 100644 --- a/lib/src/main/java/com/diffplug/spotless/FilterByFileFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FilterByFileFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,19 @@ package com.diffplug.spotless; import java.io.File; +import java.util.List; import java.util.Objects; import javax.annotation.Nullable; -final class FilterByFileFormatterStep implements FormatterStep { - private final FormatterStep delegateStep; +final class FilterByFileFormatterStep extends DelegateFormatterStep { private final SerializableFileFilter filter; FilterByFileFormatterStep(FormatterStep delegateStep, SerializableFileFilter filter) { - this.delegateStep = Objects.requireNonNull(delegateStep); + super(delegateStep); this.filter = Objects.requireNonNull(filter); } - @Override - public String getName() { - return delegateStep.getName(); - } - @Override public @Nullable String format(String raw, File file) throws Exception { Objects.requireNonNull(raw, "raw"); @@ -45,6 +40,17 @@ public String getName() { } } + @Override + public List lint(String content, File file) throws Exception { + Objects.requireNonNull(content, "content"); + Objects.requireNonNull(file, "file"); + if (filter.accept(file)) { + return delegateStep.lint(content, file); + } else { + return List.of(); + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index d1f8abd3ee..43846a2c9e 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 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,7 @@ package com.diffplug.spotless; import java.io.IOException; +import java.io.Serializable; import java.nio.charset.Charset; import java.util.Objects; import java.util.regex.Matcher; @@ -27,11 +28,12 @@ * Finds a foreign executable and checks its version. * If either part of that fails, it shows you why * and helps you fix it. - * + *

* Usage: {@code ForeignExe.nameAndVersion("grep", "2.5.7").confirmVersionAndGetAbsolutePath()} * will find grep, confirm that it is version 2.5.7, and then return. */ -public class ForeignExe { +public class ForeignExe implements Serializable { + private static final long serialVersionUID = 1L; private @Nullable String pathToExe; private String versionFlag = "--version"; private Pattern versionRegex = Pattern.compile("version (\\S*)"); diff --git a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicy.java b/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicy.java deleted file mode 100644 index da23f74432..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicy.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2016-2021 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 java.io.Serializable; - -/** A policy for handling exceptions in the format. */ -public interface FormatExceptionPolicy extends Serializable, NoLambda { - /** Called for every error in the formatter. */ - public void handleError(Throwable e, FormatterStep step, String relativePath); - - /** - * Returns a byte array representation of everything inside this {@code FormatExceptionPolicy}. - * - * The main purpose of this method is to ensure one can't instantiate this class with lambda - * expressions, which are notoriously difficult to serialize and deserialize properly. - */ - public byte[] toBytes(); - - /** - * A policy which rethrows subclasses of {@code Error} and logs other kinds of Exception. - */ - public static FormatExceptionPolicy failOnlyOnError() { - return new FormatExceptionPolicyLegacy(); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyLegacy.java b/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyLegacy.java deleted file mode 100644 index df95542a44..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyLegacy.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 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 java.util.logging.Level; -import java.util.logging.Logger; - -class FormatExceptionPolicyLegacy extends NoLambda.EqualityBasedOnSerialization implements FormatExceptionPolicy { - private static final long serialVersionUID = 1L; - - private static final Logger logger = Logger.getLogger(Formatter.class.getName()); - - @Override - public void handleError(Throwable e, FormatterStep step, String relativePath) { - if (e instanceof Error) { - error(e, step, relativePath); - throw ((Error) e); - } else { - warning(e, step, relativePath); - } - } - - static void error(Throwable e, FormatterStep step, String relativePath) { - logger.log(Level.SEVERE, "Step '" + step.getName() + "' found problem in '" + relativePath + "':\n" + e.getMessage(), e); - } - - static void warning(Throwable e, FormatterStep step, String relativePath) { - logger.log(Level.WARNING, "Unable to apply step '" + step.getName() + "' to '" + relativePath + "'", e); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyStrict.java b/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyStrict.java deleted file mode 100644 index 6fd8371928..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/FormatExceptionPolicyStrict.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 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 java.util.Objects; -import java.util.Set; -import java.util.TreeSet; - -/** - * A policy for handling exceptions in the format. Any exceptions will - * halt the build except for a specifically excluded path or step. - */ -public class FormatExceptionPolicyStrict extends NoLambda.EqualityBasedOnSerialization implements FormatExceptionPolicy { - private static final long serialVersionUID = 1L; - - private final Set excludeSteps = new TreeSet<>(); - private final Set excludePaths = new TreeSet<>(); - - /** Adds a step name to exclude. */ - public void excludeStep(String stepName) { - excludeSteps.add(Objects.requireNonNull(stepName)); - } - - /** Adds a relative path to exclude. */ - public void excludePath(String relativePath) { - excludePaths.add(Objects.requireNonNull(relativePath)); - } - - @Override - public void handleError(Throwable e, FormatterStep step, String relativePath) { - Objects.requireNonNull(e, "e"); - Objects.requireNonNull(step, "step"); - Objects.requireNonNull(relativePath, "relativePath"); - if (excludeSteps.contains(step.getName())) { - FormatExceptionPolicyLegacy.warning(e, step, relativePath); - } else { - if (excludePaths.contains(relativePath)) { - FormatExceptionPolicyLegacy.warning(e, step, relativePath); - } else { - FormatExceptionPolicyLegacy.error(e, step, relativePath); - throw ThrowingEx.asRuntimeRethrowError(e); - } - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/Formatter.java b/lib/src/main/java/com/diffplug/spotless/Formatter.java index 038e77fa64..0635f8dfe6 100644 --- a/lib/src/main/java/com/diffplug/spotless/Formatter.java +++ b/lib/src/main/java/com/diffplug/spotless/Formatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,42 +24,34 @@ import java.io.ObjectStreamException; import java.io.Serializable; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; -import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Formatter which performs the full formatting. */ public final class Formatter implements Serializable, AutoCloseable { private static final long serialVersionUID = 1L; + // The name is used for logging purpose. It does not convey any applicative + // purpose private LineEnding.Policy lineEndingsPolicy; private Charset encoding; - private Path rootDir; private List steps; - private FormatExceptionPolicy exceptionPolicy; - private Formatter(LineEnding.Policy lineEndingsPolicy, Charset encoding, Path rootDirectory, List steps, FormatExceptionPolicy exceptionPolicy) { + private Formatter(LineEnding.Policy lineEndingsPolicy, Charset encoding, List steps) { this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy, "lineEndingsPolicy"); this.encoding = Objects.requireNonNull(encoding, "encoding"); - this.rootDir = Objects.requireNonNull(rootDirectory, "rootDir"); this.steps = requireElementsNonNull(new ArrayList<>(steps)); - this.exceptionPolicy = Objects.requireNonNull(exceptionPolicy, "exceptionPolicy"); } // override serialize output private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(lineEndingsPolicy); out.writeObject(encoding.name()); - out.writeObject(rootDir.toString()); out.writeObject(steps); - out.writeObject(exceptionPolicy); } // override serialize input @@ -67,9 +59,7 @@ private void writeObject(ObjectOutputStream out) throws IOException { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { lineEndingsPolicy = (LineEnding.Policy) in.readObject(); encoding = Charset.forName((String) in.readObject()); - rootDir = Paths.get((String) in.readObject()); steps = (List) in.readObject(); - exceptionPolicy = (FormatExceptionPolicy) in.readObject(); } // override serialize input @@ -86,18 +76,10 @@ public Charset getEncoding() { return encoding; } - public Path getRootDir() { - return rootDir; - } - public List getSteps() { return steps; } - public FormatExceptionPolicy getExceptionPolicy() { - return exceptionPolicy; - } - public static Formatter.Builder builder() { return new Formatter.Builder(); } @@ -106,9 +88,7 @@ public static class Builder { // required parameters private LineEnding.Policy lineEndingsPolicy; private Charset encoding; - private Path rootDir; private List steps; - private FormatExceptionPolicy exceptionPolicy; private Builder() {} @@ -122,84 +102,13 @@ public Builder encoding(Charset encoding) { return this; } - public Builder rootDir(Path rootDir) { - this.rootDir = rootDir; - return this; - } - public Builder steps(List steps) { this.steps = steps; return this; } - public Builder exceptionPolicy(FormatExceptionPolicy exceptionPolicy) { - this.exceptionPolicy = exceptionPolicy; - return this; - } - public Formatter build() { - return new Formatter(lineEndingsPolicy, encoding, rootDir, steps, - exceptionPolicy == null ? FormatExceptionPolicy.failOnlyOnError() : exceptionPolicy); - } - } - - /** Returns true iff the given file's formatting is up-to-date. */ - public boolean isClean(File file) throws IOException { - Objects.requireNonNull(file); - - String raw = new String(Files.readAllBytes(file.toPath()), encoding); - String unix = LineEnding.toUnix(raw); - - // check the newlines (we can find these problems without even running the steps) - int totalNewLines = (int) unix.codePoints().filter(val -> val == '\n').count(); - int windowsNewLines = raw.length() - unix.length(); - if (lineEndingsPolicy.isUnix(file)) { - if (windowsNewLines != 0) { - return false; - } - } else { - if (windowsNewLines != totalNewLines) { - return false; - } - } - - // check the other formats - String formatted = compute(unix, file); - - // return true iff the formatted string equals the unix one - return formatted.equals(unix); - } - - /** Applies formatting to the given file. */ - public void applyTo(File file) throws IOException { - applyToAndReturnResultIfDirty(file); - } - - /** - * Applies formatting to the given file. - * - * Returns null if the file was already clean, or the - * formatted result with unix newlines if it was not. - */ - public @Nullable String applyToAndReturnResultIfDirty(File file) throws IOException { - Objects.requireNonNull(file); - - byte[] rawBytes = Files.readAllBytes(file.toPath()); - String raw = new String(rawBytes, encoding); - String rawUnix = LineEnding.toUnix(raw); - - // enforce the format - String formattedUnix = compute(rawUnix, file); - // enforce the line endings - String formatted = computeLineEndings(formattedUnix, file); - - // write out the file iff it has changed - byte[] formattedBytes = formatted.getBytes(encoding); - if (!Arrays.equals(rawBytes, formattedBytes)) { - Files.write(file.toPath(), formattedBytes, StandardOpenOption.TRUNCATE_EXISTING); - return formattedUnix; - } else { - return null; + return new Formatter(lineEndingsPolicy, encoding, steps); } } @@ -222,24 +131,63 @@ public String computeLineEndings(String unix, File file) { * is guaranteed to also have unix line endings. */ public String compute(String unix, File file) { + ValuePerStep exceptionPerStep = new ValuePerStep<>(this); + String result = computeWithLint(unix, file, exceptionPerStep); + legacyErrorBehavior(this, file, exceptionPerStep); + return result; + } + + static void legacyErrorBehavior(Formatter formatter, File file, ValuePerStep exceptionPerStep) { + for (int i = 0; i < formatter.getSteps().size(); ++i) { + Throwable exception = exceptionPerStep.get(i); + if (exception != null && exception != LintState.formatStepCausedNoChange()) { + logger.error("Step '{}' found problem in '{}':\n{}", formatter.getSteps().get(i).getName(), file.getName(), exception.getMessage(), exception); + throw ThrowingEx.asRuntimeRethrowError(exception); + } + } + } + + private static final Logger logger = LoggerFactory.getLogger(Formatter.class); + + /** + * Returns the result of calling all of the FormatterSteps, while also + * tracking any exceptions which are thrown. + *

+ * The input must have unix line endings, and the output + * is guaranteed to also have unix line endings. + *

+ * It doesn't matter what is inside `ValuePerStep`, the value at every index will be overwritten + * when the method returns. + */ + String computeWithLint(String unix, File file, ValuePerStep exceptionPerStep) { Objects.requireNonNull(unix, "unix"); Objects.requireNonNull(file, "file"); - for (FormatterStep step : steps) { + for (int i = 0; i < steps.size(); ++i) { + FormatterStep step = steps.get(i); + Throwable storeForStep; try { String formatted = step.format(unix, file); if (formatted == null) { // This probably means it was a step that only checks // for errors and doesn't actually have any fixes. // No exception was thrown so we can just continue. + storeForStep = LintState.formatStepCausedNoChange(); } else { // Should already be unix-only, but some steps might misbehave. - unix = LineEnding.toUnix(formatted); + String clean = LineEnding.toUnix(formatted); + if (clean.equals(unix)) { + storeForStep = LintState.formatStepCausedNoChange(); + } else { + storeForStep = null; + unix = LineEnding.toUnix(formatted); + } } } catch (Throwable e) { - String relativePath = rootDir.relativize(file.toPath()).toString(); - exceptionPolicy.handleError(e, step, relativePath); + // store the exception which was thrown and keep going + storeForStep = e; } + exceptionPerStep.set(i, storeForStep); } return unix; } @@ -250,9 +198,7 @@ public int hashCode() { int result = 1; result = prime * result + encoding.hashCode(); result = prime * result + lineEndingsPolicy.hashCode(); - result = prime * result + rootDir.hashCode(); result = prime * result + steps.hashCode(); - result = prime * result + exceptionPolicy.hashCode(); return result; } @@ -270,18 +216,31 @@ public boolean equals(Object obj) { Formatter other = (Formatter) obj; return encoding.equals(other.encoding) && lineEndingsPolicy.equals(other.lineEndingsPolicy) && - rootDir.equals(other.rootDir) && - steps.equals(other.steps) && - exceptionPolicy.equals(other.exceptionPolicy); + steps.equals(other.steps); } @SuppressWarnings("rawtypes") @Override public void close() { for (FormatterStep step : steps) { - if (step instanceof FormatterStepImpl.Standard) { - ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); + try { + step.close(); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); } } } + + /** + * This Sentinel reference may be used to pass string content to a Formatter or + * FormatterStep when there is no actual File to format + */ + public static final File NO_FILE_SENTINEL = new File("NO_FILE_SENTINEL"); + + static void checkNotSentinel(File file) { + if (file == Formatter.NO_FILE_SENTINEL) { + throw new IllegalArgumentException( + "This step requires the underlying file. If this is a test, use StepHarnessWithFile"); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java index 48a8e810ee..5e6c44b335 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 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,7 @@ package com.diffplug.spotless; import java.io.File; +import java.util.List; import java.util.Objects; /** @@ -32,6 +33,14 @@ default String apply(String unix, File file) throws Exception { return apply(unix); } + /** + * Calculates a list of lints against the given content. + * By default, that's just an throwables thrown by the lint. + */ + default List lint(String content, File file) throws Exception { + return List.of(); + } + /** * {@code Function} and {@code BiFunction} whose implementation * requires a resource which should be released when the function is no longer needed. @@ -42,11 +51,11 @@ interface Closeable extends FormatterFunc, AutoCloseable { /** * Dangerous way to create a {@link Closeable} from an AutoCloseable and a function. - * + *

* It's important for FormatterStep's to allocate their resources as lazily as possible. * It's easy to create a resource inside the state, and not realize that it may not be * released. It's far better to use one of the non-deprecated {@code of()} methods below. - * + *

* The bug (and its fix) which is easy to write using this method: https://github.com/diffplug/spotless/commit/7f16ecca031810b5e6e6f647e1f10a6d2152d9f4 * How the {@code of()} methods below make the correct thing easier to write and safer: https://github.com/diffplug/spotless/commit/18c10f9c93d6f18f753233d0b5f028d5f0961916 */ @@ -71,15 +80,17 @@ public String apply(String unix) throws Exception { }; } - /** @deprecated synonym for {@link #ofDangerous(AutoCloseable, FormatterFunc)} */ - @Deprecated - public static Closeable of(AutoCloseable closeable, FormatterFunc function) { - return ofDangerous(closeable, function); - } - @FunctionalInterface interface ResourceFunc { String apply(T resource, String unix) throws Exception; + + /** + * Calculates a list of lints against the given content. + * By default, that's just an throwables thrown by the lint. + */ + default List lint(T resource, String unix) throws Exception { + return List.of(); + } } /** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the format function. */ @@ -107,6 +118,10 @@ public String apply(String unix) throws Exception { @FunctionalInterface interface ResourceFuncNeedsFile { String apply(T resource, String unix, File file) throws Exception; + + default List lint(T resource, String content, File file) throws Exception { + return List.of(); + } } /** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the file-dependent format function. */ @@ -121,13 +136,18 @@ public void close() { @Override public String apply(String unix, File file) throws Exception { - FormatterStepImpl.checkNotSentinel(file); + Formatter.checkNotSentinel(file); return function.apply(resource, unix, file); } @Override public String apply(String unix) throws Exception { - return apply(unix, FormatterStepImpl.SENTINEL); + return apply(unix, Formatter.NO_FILE_SENTINEL); + } + + @Override + public List lint(String content, File file) throws Exception { + return function.lint(resource, content, file); } }; } @@ -150,13 +170,13 @@ interface NeedsFile extends FormatterFunc { @Override default String apply(String unix, File file) throws Exception { - FormatterStepImpl.checkNotSentinel(file); + Formatter.checkNotSentinel(file); return applyWithFile(unix, file); } @Override default String apply(String unix) throws Exception { - return apply(unix, FormatterStepImpl.SENTINEL); + return apply(unix, Formatter.NO_FILE_SENTINEL); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterProperties.java b/lib/src/main/java/com/diffplug/spotless/FormatterProperties.java index 81fa6a93cf..5ea32f8f99 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterProperties.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,12 @@ import static com.diffplug.spotless.MoreIterables.toNullHostileList; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -75,6 +77,25 @@ public static FormatterProperties from(Iterable files) throws IllegalArgum return properties; } + public static FormatterProperties fromPropertiesContent(Iterable content) throws IllegalArgumentException { + List nonNullElements = toNullHostileList(content); + FormatterProperties properties = new FormatterProperties(); + nonNullElements.forEach(contentElement -> { + try (InputStream is = new ByteArrayInputStream(contentElement.getBytes(StandardCharsets.UTF_8))) { + properties.properties.load(is); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to load properties: " + contentElement); + } + }); + return properties; + } + + public static FormatterProperties merge(Properties... properties) { + FormatterProperties merged = new FormatterProperties(); + List.of(properties).stream().forEach((source) -> merged.properties.putAll(source)); + return merged; + } + /** * Import settings from given file. New settings (with the same ID/key) * override existing once. diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java index 5729f676b2..a26ade4a41 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,20 @@ import java.io.File; import java.io.Serializable; +import java.util.List; import java.util.Objects; import javax.annotation.Nullable; /** * An implementation of this class specifies a single step in a formatting process. - * + *

* The input is guaranteed to have unix-style newlines, and the output is required * to not introduce any windows-style newlines as well. */ -public interface FormatterStep extends Serializable { +public interface FormatterStep extends Serializable, AutoCloseable { /** The name of the step, for debugging purposes. */ - public String getName(); + String getName(); /** * Returns a formatted version of the given content. @@ -37,56 +38,98 @@ public interface FormatterStep extends Serializable { * @param rawUnix * the content to format, guaranteed to have unix-style newlines ('\n'); never null * @param file - * the file which {@code rawUnix} was obtained from; never null. Pass an empty file using - * {@code new File("")} if and only if no file is actually associated with {@code rawUnix} + * the file which {@code rawUnix} was obtained from; never null. Pass the reference + * {@link Formatter#NO_FILE_SENTINEL} if and only if no file is actually associated with {@code rawUnix} * @return the formatted content, guaranteed to only have unix-style newlines; may return null * if the formatter step doesn't have any changes to make * @throws Exception if the formatter step experiences a problem */ - public @Nullable String format(String rawUnix, File file) throws Exception; + @Nullable + String format(String rawUnix, File file) throws Exception; /** - * Returns a new FormatterStep which will only apply its changes - * to files which pass the given filter. + * Returns a list of lints against the given file content * + * @param content + * the content to check + * @param file + * the file which {@code content} was obtained from; never null. Pass an empty file using + * {@code new File("")} if and only if no file is actually associated with {@code content} + * @return a list of lints + * @throws Exception if the formatter step experiences a problem + */ + @Nullable + default List lint(String content, File file) throws Exception { + return List.of(); + } + + /** + * Returns a new {@code FormatterStep} which, observing the value of {@code formatIfMatches}, + * will only apply, or not, its changes to files which pass the given filter. + * + * @param onMatch + * determines if matches are included or excluded * @param contentPattern - * java regular expression used to filter out files which content doesn't contain pattern + * java regular expression used to filter in or out files which content contain pattern * @return FormatterStep */ - public default FormatterStep filterByContentPattern(String contentPattern) { - return new FilterByContentPatternFormatterStep(this, contentPattern); + default FormatterStep filterByContent(OnMatch onMatch, String contentPattern) { + return new FilterByContentPatternFormatterStep(this, onMatch, contentPattern); } /** * Returns a new FormatterStep which will only apply its changes * to files which pass the given filter. - * + *

* The provided filter must be serializable. */ - public default FormatterStep filterByFile(SerializableFileFilter filter) { + default FormatterStep filterByFile(SerializableFileFilter filter) { return new FilterByFileFormatterStep(this, filter); } /** - * Implements a FormatterStep in a strict way which guarantees correct and lazy implementation - * of up-to-date checks. This maximizes performance for cases where the FormatterStep is not - * actually needed (e.g. don't load eclipse setting file unless this step is actually running) - * while also ensuring that gradle can detect changes in a step's settings to determine that - * it needs to rerun a format. + * @param name + * The name of the formatter step. + * @param roundtripInit + * If the step has any state, this supplier will calculate it lazily. The supplier doesn't + * have to be serializable, but the result it calculates needs to be serializable. + * @param equalityFunc + * A pure serializable function (method reference recommended) which takes the result of `roundtripInit`, + * and returns a serializable object whose serialized representation will be used for `.equals` and + * `.hashCode` of the FormatterStep. + * @param formatterFunc + * A pure serializable function (method reference recommended) which takes the result of `equalityFunc`, + * and returns a `FormatterFunc` which will be used for the actual formatting. + * @return A FormatterStep which can be losslessly roundtripped through the java serialization machinery. */ - abstract class Strict extends LazyForwardingEquality implements FormatterStep { - private static final long serialVersionUID = 1L; - - /** - * Implements the formatting function strictly in terms - * of the input data and the result of {@link #calculateState()}. - */ - protected abstract String format(State state, String rawUnix, File file) throws Exception; + static FormatterStep createLazy( + String name, + ThrowingEx.Supplier roundtripInit, + SerializedFunction equalityFunc, + SerializedFunction formatterFunc) { + return new FormatterStepSerializationRoundtrip<>(name, roundtripInit, equalityFunc, formatterFunc); + } - @Override - public final String format(String rawUnix, File file) throws Exception { - return format(state(), rawUnix, file); - } + /** + * @param name + * The name of the formatter step. + * @param roundTrip + * The roundtrip serializable state of the step. + * @param equalityFunc + * A pure serializable function (method reference recommended) which takes the result of `roundTrip`, + * and returns a serializable object whose serialized representation will be used for `.equals` and + * `.hashCode` of the FormatterStep. + * @param formatterFunc + * A pure serializable function (method reference recommended) which takes the result of `equalityFunc`, + * and returns a `FormatterFunc` which will be used for the actual formatting. + * @return A FormatterStep which can be losslessly roundtripped through the java serialization machinery. + */ + static FormatterStep create( + String name, + RoundtripState roundTrip, + SerializedFunction equalityFunc, + SerializedFunction formatterFunc) { + return createLazy(name, () -> roundTrip, equalityFunc, formatterFunc); } /** @@ -100,11 +143,11 @@ public final String format(String rawUnix, File file) throws Exception { * only the state supplied by state and nowhere else. * @return A FormatterStep */ - public static FormatterStep createLazy( + static FormatterStep createLazy( String name, ThrowingEx.Supplier stateSupplier, - ThrowingEx.Function stateToFormatter) { - return new FormatterStepImpl.Standard<>(name, stateSupplier, stateToFormatter); + SerializedFunction stateToFormatter) { + return createLazy(name, stateSupplier, SerializedFunction.identity(), stateToFormatter); } /** @@ -117,41 +160,11 @@ public static FormatterStep createLazy( * only the state supplied by state and nowhere else. * @return A FormatterStep */ - public static FormatterStep create( + static FormatterStep create( String name, State state, - ThrowingEx.Function stateToFormatter) { + SerializedFunction stateToFormatter) { Objects.requireNonNull(state, "state"); return createLazy(name, () -> state, stateToFormatter); } - - /** - * @param name - * The name of the formatter step - * @param functionSupplier - * A supplier which will lazily generate the function - * used by the formatter step - * @return A FormatterStep which will never report that it is up-to-date, because - * it is not equal to the serialized representation of itself. - */ - public static FormatterStep createNeverUpToDateLazy( - String name, - ThrowingEx.Supplier functionSupplier) { - return new FormatterStepImpl.NeverUpToDate(name, functionSupplier); - } - - /** - * @param name - * The name of the formatter step - * @param function - * The function used by the formatter step - * @return A FormatterStep which will never report that it is up-to-date, because - * it is not equal to the serialized representation of itself. - */ - public static FormatterStep createNeverUpToDate( - String name, - FormatterFunc function) { - Objects.requireNonNull(function, "function"); - return createNeverUpToDateLazy(name, () -> function); - } } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepEqualityOnStateSerialization.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepEqualityOnStateSerialization.java new file mode 100644 index 0000000000..e42e0cb4f9 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepEqualityOnStateSerialization.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016-2024 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 java.io.File; +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Standard implementation of FormatterStep which cleanly enforces + * separation of a lazily computed "state" object whose serialized form + * is used as the basis for equality and hashCode, which is separate + * from the serialized form of the step itself, which can include absolute paths + * and such without interfering with buildcache keys. + */ +@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") +abstract class FormatterStepEqualityOnStateSerialization implements FormatterStep, Serializable { + private static final long serialVersionUID = 1L; + + protected abstract State stateSupplier() throws Exception; + + protected abstract FormatterFunc stateToFormatter(State state) throws Exception; + + private transient FormatterFunc formatter; + private transient State stateInternal; + private transient byte[] serializedStateInternal; + + @Override + public String format(String rawUnix, File file) throws Exception { + if (formatter == null) { + formatter = stateToFormatter(state()); + } + return formatter.apply(rawUnix, file); + } + + @Override + public List lint(String content, File file) throws Exception { + if (formatter == null) { + formatter = stateToFormatter(state()); + } + return formatter.lint(content, file); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } else if (getClass() != o.getClass()) { + return false; + } else { + return Arrays.equals(serializedState(), ((FormatterStepEqualityOnStateSerialization) o).serializedState()); + } + } + + @Override + public int hashCode() { + return Arrays.hashCode(serializedState()); + } + + @Override + public void close() { + if (formatter instanceof FormatterFunc.Closeable) { + ((FormatterFunc.Closeable) formatter).close(); + formatter = null; + } + } + + private State state() throws Exception { + if (stateInternal == null) { + stateInternal = stateSupplier(); + } + return stateInternal; + } + + private byte[] serializedState() { + if (serializedStateInternal == null) { + serializedStateInternal = ThrowingEx.get(() -> LazyForwardingEquality.toBytes(state())); + } + return serializedStateInternal; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java deleted file mode 100644 index 061aff6af9..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2016-2020 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 java.io.File; -import java.io.Serializable; -import java.util.Objects; -import java.util.Random; - -import com.diffplug.spotless.FormatterStep.Strict; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Standard implementation of FormatExtension which cleanly enforces - * separation of serializable configuration and a pure format function. - * - * Not an inner-class of FormatterStep so that it can stay entirely private - * from the API. - */ -@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") -abstract class FormatterStepImpl extends Strict { - private static final long serialVersionUID = 1L; - - /** Transient because only the state matters. */ - final transient String name; - - /** Transient because only the state matters. */ - final transient ThrowingEx.Supplier stateSupplier; - - FormatterStepImpl(String name, ThrowingEx.Supplier stateSupplier) { - this.name = Objects.requireNonNull(name); - this.stateSupplier = Objects.requireNonNull(stateSupplier); - } - - @Override - public String getName() { - return name; - } - - @Override - protected State calculateState() throws Exception { - return stateSupplier.get(); - } - - static final class Standard extends FormatterStepImpl { - private static final long serialVersionUID = 1L; - - final transient ThrowingEx.Function stateToFormatter; - transient FormatterFunc formatter; // initialized lazily - - Standard(String name, ThrowingEx.Supplier stateSupplier, ThrowingEx.Function stateToFormatter) { - super(name, stateSupplier); - this.stateToFormatter = Objects.requireNonNull(stateToFormatter); - } - - @Override - protected String format(State state, String rawUnix, File file) throws Exception { - Objects.requireNonNull(state, "state"); - Objects.requireNonNull(rawUnix, "rawUnix"); - Objects.requireNonNull(file, "file"); - if (formatter == null) { - formatter = stateToFormatter.apply(state()); - } - return formatter.apply(rawUnix, file); - } - - void cleanupFormatterFunc() { - if (formatter instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatter).close(); - formatter = null; - } - } - } - - /** Formatter which is equal to itself, but not to any other Formatter. */ - static class NeverUpToDate extends FormatterStepImpl { - private static final long serialVersionUID = 1L; - - private static final Random RANDOM = new Random(); - - final transient ThrowingEx.Supplier formatterSupplier; - transient FormatterFunc formatter; // initialized lazily - - NeverUpToDate(String name, ThrowingEx.Supplier formatterSupplier) { - super(name, RANDOM::nextInt); - this.formatterSupplier = Objects.requireNonNull(formatterSupplier, "formatterSupplier"); - } - - @Override - protected String format(Integer state, String rawUnix, File file) throws Exception { - if (formatter == null) { - formatter = formatterSupplier.get(); - if (formatter instanceof FormatterFunc.Closeable) { - throw new AssertionError("NeverUpToDate does not support FormatterFunc.Closeable. See https://github.com/diffplug/spotless/pull/284"); - } - } - return formatter.apply(rawUnix, file); - } - } - - /** A dummy SENTINEL file. */ - static final File SENTINEL = new File(""); - - static void checkNotSentinel(File file) { - if (file == SENTINEL) { - throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java new file mode 100644 index 0000000000..f577760b74 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023-2025 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 java.io.IOException; +import java.io.Serializable; +import java.util.Objects; + +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +final class FormatterStepSerializationRoundtrip extends FormatterStepEqualityOnStateSerialization { + private static final long serialVersionUID = 1L; + private final String name; + @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "HackClone") + private final transient ThrowingEx.Supplier initializer; + private @Nullable RoundtripState roundtripStateInternal; + private @Nullable EqualityState equalityStateInternal; + private final SerializedFunction equalityStateExtractor; + private final SerializedFunction equalityStateToFormatter; + + FormatterStepSerializationRoundtrip(String name, ThrowingEx.Supplier initializer, SerializedFunction equalityStateExtractor, SerializedFunction equalityStateToFormatter) { + this.name = name; + this.initializer = initializer; + this.equalityStateExtractor = equalityStateExtractor; + this.equalityStateToFormatter = equalityStateToFormatter; + } + + @Override + public String getName() { + return name; + } + + private RoundtripState roundtripStateSupplier() throws Exception { + if (roundtripStateInternal == null) { + roundtripStateInternal = initializer.get(); + } + return roundtripStateInternal; + } + + @Override + protected EqualityState stateSupplier() throws Exception { + if (equalityStateInternal == null) { + equalityStateInternal = equalityStateExtractor.apply(roundtripStateSupplier()); + } + return equalityStateInternal; + } + + @Override + protected FormatterFunc stateToFormatter(EqualityState equalityState) throws Exception { + return equalityStateToFormatter.apply(equalityState); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + if (initializer == null) { + // then this instance was created by Gradle's ConfigurationCacheHackList and the following will hold true + if (roundtripStateInternal == null && equalityStateInternal == null) { + throw new IllegalStateException("If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null, and neither was"); + } + } else { + // this was a normal instance, which means we need to encode to roundtripStateInternal (since the initializer might not be serializable) + // and there's no reason to keep equalityStateInternal since we can always recompute it + if (roundtripStateInternal == null) { + roundtripStateInternal = ThrowingEx.get(this::roundtripStateSupplier); + } + equalityStateInternal = null; + } + out.defaultWriteObject(); + } + + HackClone hackClone(boolean optimizeForEquality) { + return new HackClone<>(this, optimizeForEquality); + } + + /** + * This class has one setting (optimizeForEquality) and two pieces of data + * - the original step, which is marked transient so it gets discarded during serialization + * - the cleaned step, which is lazily created during serialization, and the serialized form is optimized for either equality or roundtrip integrity + * + * It works in conjunction with ConfigurationCacheHackList to allow Spotless to work with all of Gradle's cache systems. + */ + static class HackClone implements Serializable { + private static final long serialVersionUID = 1L; + @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "HackClone") + transient FormatterStepSerializationRoundtrip original; + boolean optimizeForEquality; + @Nullable + FormatterStepSerializationRoundtrip cleaned; + + HackClone(@Nullable FormatterStepSerializationRoundtrip original, boolean optimizeForEquality) { + this.original = original; + this.optimizeForEquality = optimizeForEquality; + } + + @SuppressFBWarnings(value = "NP_NONNULL_PARAM_VIOLATION", justification = "HackClone") + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + if (cleaned == null) { + cleaned = new FormatterStepSerializationRoundtrip(original.name, null, original.equalityStateExtractor, original.equalityStateToFormatter); + if (optimizeForEquality) { + cleaned.equalityStateInternal = ThrowingEx.get(original::stateSupplier); + } else { + cleaned.roundtripStateInternal = ThrowingEx.get(original::roundtripStateSupplier); + } + } + out.defaultWriteObject(); + } + + public FormatterStep rehydrate() { + return original != null ? original : Objects.requireNonNull(cleaned, "how is clean null if this has been serialized?"); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + HackClone that = (HackClone) o; + return optimizeForEquality == that.optimizeForEquality && rehydrate().equals(that.rehydrate()); + } + + @Override + public int hashCode() { + return rehydrate().hashCode() ^ Boolean.hashCode(optimizeForEquality); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/JarState.java b/lib/src/main/java/com/diffplug/spotless/JarState.java index abe595e608..76dee4f438 100644 --- a/lib/src/main/java/com/diffplug/spotless/JarState.java +++ b/lib/src/main/java/com/diffplug/spotless/JarState.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 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 java.io.File; import java.io.IOException; +import java.io.ObjectStreamException; import java.io.Serializable; import java.net.URI; import java.net.URL; @@ -25,25 +26,85 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; -import java.util.TreeSet; import java.util.stream.Collectors; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Grabs a jar and its dependencies from maven, * and makes it easy to access the collection in * a classloader. - * + *

* Serializes the full state of the jar, so it can * catch changes in a SNAPSHOT version. */ public final class JarState implements Serializable { + + private static final Logger logger = LoggerFactory.getLogger(JarState.class); + + // Let the classloader be overridden for tools using different approaches to classloading + @Nullable + private static ClassLoader forcedClassLoader = null; + + /** Overrides the classloader used by all JarStates. */ + public static void setForcedClassLoader(@Nullable ClassLoader forcedClassLoader) { + if (!Objects.equals(JarState.forcedClassLoader, forcedClassLoader)) { + logger.info("Overriding the forced classloader for JarState from {} to {}", JarState.forcedClassLoader, forcedClassLoader); + } + JarState.forcedClassLoader = forcedClassLoader; + } + + /** A lazily evaluated JarState, which becomes a set of files when serialized. */ + public static class Promised implements Serializable { + private static final long serialVersionUID = 1L; + private final transient ThrowingEx.Supplier supplier; + private FileSignature.Promised cached; + + public Promised(ThrowingEx.Supplier supplier) { + this.supplier = supplier; + } + + public JarState get() { + try { + if (cached == null) { + JarState result = supplier.get(); + cached = result.fileSignature.asPromise(); + return result; + } + return new JarState(cached.get()); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); + } + } + + // override serialize output + private void writeObject(java.io.ObjectOutputStream out) + throws IOException { + get(); + out.defaultWriteObject(); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + } + + private void readObjectNoData() throws ObjectStreamException { + throw new UnsupportedOperationException(); + } + } + + public static Promised promise(ThrowingEx.Supplier supplier) { + return new Promised(supplier); + } + private static final long serialVersionUID = 1L; - private final Set mavenCoordinates; private final FileSignature fileSignature; - private JarState(Collection mavenCoordinates, FileSignature fileSignature) { - this.mavenCoordinates = new TreeSet(mavenCoordinates); + private JarState(FileSignature fileSignature) { this.fileSignature = fileSignature; } @@ -70,7 +131,13 @@ private static JarState provisionWithTransitives(boolean withTransitives, Collec throw new NoSuchElementException("Resolved to an empty result: " + mavenCoordinates.stream().collect(Collectors.joining(", "))); } FileSignature fileSignature = FileSignature.signAsSet(jars); - return new JarState(mavenCoordinates, fileSignature); + return new JarState(fileSignature); + } + + /** Wraps the given collection of a files as a JarState, maintaining the order in the Collection. */ + public static JarState preserveOrder(Collection jars) throws IOException { + FileSignature fileSignature = FileSignature.signAsList(jars); + return new JarState(fileSignature); } URL[] jarUrls() { @@ -78,31 +145,36 @@ URL[] jarUrls() { } /** - * Returns a classloader containing the only jars in this JarState. + * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader()}) or a classloader containing the only jars in this JarState. * Look-up of classes in the {@code org.slf4j} package * are not taken from the JarState, but instead redirected to the class loader of this class to enable * passthrough logging. *
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}. + * + * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader) */ public ClassLoader getClassLoader() { + if (forcedClassLoader != null) { + return forcedClassLoader; + } return SpotlessCache.instance().classloader(this); } /** - * Returns a classloader containing the only jars in this JarState. + * Returns either a forcedClassloader ({@code JarState.setForcedClassLoader}) or a classloader containing the only jars in this JarState. * Look-up of classes in the {@code org.slf4j} package * are not taken from the JarState, but instead redirected to the class loader of this class to enable * passthrough logging. *
- * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}. + * The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache} + * + * @see com.diffplug.spotless.JarState#setForcedClassLoader(ClassLoader) */ public ClassLoader getClassLoader(Serializable key) { + if (forcedClassLoader != null) { + return forcedClassLoader; + } return SpotlessCache.instance().classloader(key, this); } - - /** Returns unmodifiable view on sorted Maven coordinates */ - public Set getMavenCoordinates() { - return Collections.unmodifiableSet(mavenCoordinates); - } } diff --git a/lib/src/main/java/com/diffplug/spotless/Jvm.java b/lib/src/main/java/com/diffplug/spotless/Jvm.java index e22e0c6339..3e8339644a 100644 --- a/lib/src/main/java/com/diffplug/spotless/Jvm.java +++ b/lib/src/main/java/com/diffplug/spotless/Jvm.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,10 +61,13 @@ public static int version() { * @param Version type of formatter */ public static class Support { + static final String LINT_CODE = "jvm-version"; + private final String fmtName; private final Comparator fmtVersionComparator; - private final NavigableMap jvm2fmtVersion; - private final NavigableMap fmt2jvmVersion; + private final NavigableMap jvm2fmtMaxVersion; + private final NavigableMap jvm2fmtMinVersion; + private final NavigableMap fmtMaxVersion2jvmVersion; private Support(String fromatterName) { this(fromatterName, new SemanticVersionComparator()); @@ -73,40 +76,60 @@ private Support(String fromatterName) { private Support(String formatterName, Comparator formatterVersionComparator) { fmtName = formatterName; fmtVersionComparator = formatterVersionComparator; - jvm2fmtVersion = new TreeMap(); - fmt2jvmVersion = new TreeMap(formatterVersionComparator); + jvm2fmtMaxVersion = new TreeMap<>(); + jvm2fmtMinVersion = new TreeMap<>(); + fmtMaxVersion2jvmVersion = new TreeMap<>(formatterVersionComparator); } /** - * Add supported formatter version + * Add maximum supported formatter version * @param minimumJvmVersion Minimum Java version required * @param maxFormatterVersion Maximum formatter version supported by the Java version * @return this */ public Support add(int minimumJvmVersion, V maxFormatterVersion) { Objects.requireNonNull(maxFormatterVersion); - if (null != jvm2fmtVersion.put(minimumJvmVersion, maxFormatterVersion)) { + if (null != jvm2fmtMaxVersion.put(minimumJvmVersion, maxFormatterVersion)) { throw new IllegalArgumentException(String.format("Added duplicate entry for JVM %d+.", minimumJvmVersion)); } - if (null != fmt2jvmVersion.put(maxFormatterVersion, minimumJvmVersion)) { + if (null != fmtMaxVersion2jvmVersion.put(maxFormatterVersion, minimumJvmVersion)) { throw new IllegalArgumentException(String.format("Added duplicate entry for formatter version %s.", maxFormatterVersion)); } + verifyVersionRangesDoNotIntersect(jvm2fmtMaxVersion, minimumJvmVersion, maxFormatterVersion); + return this; + } + + public Support addMin(int minimumJvmVersion, V minFormatterVersion) { + Objects.requireNonNull(minFormatterVersion); + if (null != jvm2fmtMinVersion.put(minimumJvmVersion, minFormatterVersion)) { + throw new IllegalArgumentException(String.format("Added duplicate entry for JVM %d+.", minimumJvmVersion)); + } + verifyVersionRangesDoNotIntersect(jvm2fmtMinVersion, minimumJvmVersion, minFormatterVersion); + return this; + } + + private void verifyVersionRangesDoNotIntersect(NavigableMap jvm2fmtVersion, int minimumJvmVersion, V formatterVersion) { Map.Entry lower = jvm2fmtVersion.lowerEntry(minimumJvmVersion); - if ((null != lower) && (fmtVersionComparator.compare(maxFormatterVersion, lower.getValue()) <= 0)) { - throw new IllegalArgumentException(String.format("%d/%s should be lower than %d/%s", minimumJvmVersion, maxFormatterVersion, lower.getKey(), lower.getValue())); + if ((null != lower) && (fmtVersionComparator.compare(formatterVersion, lower.getValue()) <= 0)) { + throw new IllegalArgumentException(String.format("%d/%s should be lower than %d/%s", minimumJvmVersion, formatterVersion, lower.getKey(), lower.getValue())); } Map.Entry higher = jvm2fmtVersion.higherEntry(minimumJvmVersion); - if ((null != higher) && (fmtVersionComparator.compare(maxFormatterVersion, higher.getValue()) >= 0)) { - throw new IllegalArgumentException(String.format("%d/%s should be higher than %d/%s", minimumJvmVersion, maxFormatterVersion, higher.getKey(), higher.getValue())); + if ((null != higher) && (fmtVersionComparator.compare(formatterVersion, higher.getValue()) >= 0)) { + throw new IllegalArgumentException(String.format("%d/%s should be higher than %d/%s", minimumJvmVersion, formatterVersion, higher.getKey(), higher.getValue())); } - return this; } /** @return Highest formatter version recommended for this JVM (null, if JVM not supported) */ @Nullable public V getRecommendedFormatterVersion() { - Integer configuredJvmVersionOrNull = jvm2fmtVersion.floorKey(Jvm.version()); - return (null == configuredJvmVersionOrNull) ? null : jvm2fmtVersion.get(configuredJvmVersionOrNull); + Integer configuredJvmVersionOrNull = jvm2fmtMaxVersion.floorKey(Jvm.version()); + return (null == configuredJvmVersionOrNull) ? null : jvm2fmtMaxVersion.get(configuredJvmVersionOrNull); + } + + @Nullable + public V getMinimumRequiredFormatterVersion() { + Integer configuredJvmVersionOrNull = jvm2fmtMinVersion.floorKey(Jvm.version()); + return (null == configuredJvmVersionOrNull) ? null : jvm2fmtMinVersion.get(configuredJvmVersionOrNull); } /** @@ -118,15 +141,22 @@ public void assertFormatterSupported(V formatterVersion) { Objects.requireNonNull(formatterVersion); String error = buildUnsupportedFormatterMessage(formatterVersion); if (!error.isEmpty()) { - throw new IllegalArgumentException(error); + throw Lint.atUndefinedLine(LINT_CODE, error).shortcut(); } } private String buildUnsupportedFormatterMessage(V fmtVersion) { + // check if the jvm version is to low for the formatter version int requiredJvmVersion = getRequiredJvmVersion(fmtVersion); if (Jvm.version() < requiredJvmVersion) { return buildUpgradeJvmMessage(fmtVersion) + "Upgrade your JVM or try " + toString(); } + // check if the formatter version is too low for the jvm version + V minimumFormatterVersion = getMinimumRequiredFormatterVersion(); + if ((null != minimumFormatterVersion) && (fmtVersionComparator.compare(fmtVersion, minimumFormatterVersion) < 0)) { + return String.format("You are running Spotless on JVM %d. This requires %s of at least %s (you are using %s).%n", Jvm.version(), fmtName, minimumFormatterVersion, fmtVersion); + } + // otherwise all is well return ""; } @@ -137,7 +167,7 @@ private String buildUpgradeJvmMessage(V fmtVersion) { if (null != recommendedFmtVersionOrNull) { builder.append(String.format(", which limits you to %s %s.%n", fmtName, recommendedFmtVersionOrNull)); } else { - Entry nextFmtVersionOrNull = fmt2jvmVersion.ceilingEntry(fmtVersion); + Entry nextFmtVersionOrNull = fmtMaxVersion2jvmVersion.ceilingEntry(fmtVersion); if (null != nextFmtVersionOrNull) { builder.append(String.format(". %s %s requires JVM %d+", fmtName, fmtVersion, nextFmtVersionOrNull.getValue())); } @@ -147,12 +177,12 @@ private String buildUpgradeJvmMessage(V fmtVersion) { } private int getRequiredJvmVersion(V fmtVersion) { - Entry entry = fmt2jvmVersion.ceilingEntry(fmtVersion); + Entry entry = fmtMaxVersion2jvmVersion.ceilingEntry(fmtVersion); if (null == entry) { - entry = fmt2jvmVersion.lastEntry(); + entry = fmtMaxVersion2jvmVersion.lastEntry(); } if (null != entry) { - V maxKnownFmtVersion = jvm2fmtVersion.get(entry.getValue()); + V maxKnownFmtVersion = jvm2fmtMaxVersion.get(entry.getValue()); if (fmtVersionComparator.compare(fmtVersion, maxKnownFmtVersion) <= 0) { return entry.getValue(); } @@ -170,15 +200,15 @@ public FormatterFunc suggestLaterVersionOnError(V formatterVersion, FormatterFun Objects.requireNonNull(formatterVersion); Objects.requireNonNull(originalFunc); final String hintUnsupportedProblem = buildUnsupportedFormatterMessage(formatterVersion); - final String proposeDiffererntFormatter = hintUnsupportedProblem.isEmpty() ? buildUpgradeFormatterMessage(formatterVersion) : hintUnsupportedProblem; - return proposeDiffererntFormatter.isEmpty() ? originalFunc : new FormatterFunc() { + final String proposeDifferentFormatter = hintUnsupportedProblem.isEmpty() ? buildUpgradeFormatterMessage(formatterVersion) : hintUnsupportedProblem; + return proposeDifferentFormatter.isEmpty() ? originalFunc : new FormatterFunc() { @Override public String apply(String unix, File file) throws Exception { try { return originalFunc.apply(unix, file); } catch (Exception e) { - throw new Exception(proposeDiffererntFormatter, e); + throw new Exception(proposeDifferentFormatter, e); } } @@ -187,7 +217,7 @@ public String apply(String input) throws Exception { try { return originalFunc.apply(input); } catch (Exception e) { - throw new Exception(proposeDiffererntFormatter, e); + throw new Exception(proposeDifferentFormatter, e); } } @@ -196,15 +226,25 @@ public String apply(String input) throws Exception { private String buildUpgradeFormatterMessage(V fmtVersion) { StringBuilder builder = new StringBuilder(); + // check if the formatter is not supported on this jvm + V minimumFormatterVersion = getMinimumRequiredFormatterVersion(); V recommendedFmtVersionOrNull = getRecommendedFormatterVersion(); - if (null != recommendedFmtVersionOrNull && (fmtVersionComparator.compare(fmtVersion, recommendedFmtVersionOrNull) < 0)) { - builder.append(String.format("You are not using latest version on JVM %d+.%n", getRequiredJvmVersion(recommendedFmtVersionOrNull))); - builder.append(String.format("Try to upgrade to %s %s, which may have fixed this problem.", fmtName, getRecommendedFormatterVersion())); + if ((null != minimumFormatterVersion) && (fmtVersionComparator.compare(fmtVersion, minimumFormatterVersion) < 0)) { + builder.append(String.format("You are running Spotless on JVM %d. This requires %s of at least %s.%n", Jvm.version(), fmtName, minimumFormatterVersion)); + builder.append(String.format("You are using %s %s.%n", fmtName, fmtVersion)); + if (null != recommendedFmtVersionOrNull) { + builder.append(String.format("%s %s is the recommended version, which may have fixed this problem.%n", fmtName, recommendedFmtVersionOrNull)); + } + // check if the formatter is outdated on this jvm + } else if (null != recommendedFmtVersionOrNull && (fmtVersionComparator.compare(fmtVersion, recommendedFmtVersionOrNull) < 0)) { + builder.append(String.format("%s %s is currently being used, but outdated.%n", fmtName, fmtVersion)); + builder.append(String.format("%s %s is the recommended version, which may have fixed this problem.%n", fmtName, recommendedFmtVersionOrNull)); + builder.append(String.format("%s %s requires JVM %d+.", fmtName, recommendedFmtVersionOrNull, getRequiredJvmVersion(recommendedFmtVersionOrNull))); } else { - V higherFormatterVersionOrNull = fmt2jvmVersion.higherKey(fmtVersion); + V higherFormatterVersionOrNull = fmtMaxVersion2jvmVersion.higherKey(fmtVersion); if (null != higherFormatterVersionOrNull) { builder.append(buildUpgradeJvmMessage(fmtVersion)); - Integer higherJvmVersion = fmt2jvmVersion.get(higherFormatterVersionOrNull); + Integer higherJvmVersion = fmtMaxVersion2jvmVersion.get(higherFormatterVersionOrNull); builder.append(String.format("If you upgrade your JVM to %d+, then you can use %s %s, which may have fixed this problem.", higherJvmVersion, fmtName, higherFormatterVersionOrNull)); } } @@ -214,7 +254,7 @@ private String buildUpgradeFormatterMessage(V fmtVersion) { @Override public String toString() { return String.format("%s alternatives:%n", fmtName) + - jvm2fmtVersion.entrySet().stream().map( + jvm2fmtMaxVersion.entrySet().stream().map( e -> String.format("- Version %s requires JVM %d+", e.getValue(), e.getKey())).collect(Collectors.joining(System.lineSeparator())); } @@ -242,7 +282,11 @@ public int compare(V version0, V version1) { private static int[] convert(V versionObject) { try { - return Arrays.asList(versionObject.toString().split("\\.")).stream().mapToInt(Integer::parseInt).toArray(); + String versionString = versionObject.toString(); + if (versionString.endsWith("-SNAPSHOT")) { + versionString = versionString.substring(0, versionString.length() - "-SNAPSHOT".length()); + } + return Arrays.asList(versionString.split("\\.")).stream().mapToInt(Integer::parseInt).toArray(); } catch (Exception e) { throw new IllegalArgumentException(String.format("Not a semantic version: %s", versionObject), e); } diff --git a/lib/src/main/java/com/diffplug/spotless/LazyArgLogger.java b/lib/src/main/java/com/diffplug/spotless/LazyArgLogger.java new file mode 100644 index 0000000000..e3456a2317 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/LazyArgLogger.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 java.util.function.Supplier; + +/** + * This is a utility class to allow for lazy evaluation of arguments to be passed to a logger + * and thus avoid unnecessary computation of the arguments if the log level is not enabled. + */ +public final class LazyArgLogger { + + private final Supplier argSupplier; + + private LazyArgLogger(Supplier argSupplier) { + this.argSupplier = argSupplier; + } + + public static LazyArgLogger lazy(Supplier argSupplier) { + return new LazyArgLogger(argSupplier); + } + + @Override + public String toString() { + return String.valueOf(argSupplier.get()); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/LazyForwardingEquality.java b/lib/src/main/java/com/diffplug/spotless/LazyForwardingEquality.java index 05e758d586..f81570cedf 100644 --- a/lib/src/main/java/com/diffplug/spotless/LazyForwardingEquality.java +++ b/lib/src/main/java/com/diffplug/spotless/LazyForwardingEquality.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2025 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,7 @@ */ package com.diffplug.spotless; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; @@ -41,7 +42,7 @@ public abstract class LazyForwardingEquality implements /** * This function is guaranteed to be called at most once. * If the state is never required, then it will never be called at all. - * + *

* Throws exception because it's likely that there will be some IO going on. */ protected abstract T calculateState() throws Exception; @@ -111,4 +112,27 @@ static byte[] toBytes(Serializable obj) { } return byteOutput.toByteArray(); } + + static Object fromBytes(byte[] bytes) { + ByteArrayInputStream byteOutput = new ByteArrayInputStream(bytes); + try (ObjectInputStream objectOutput = new ObjectInputStream(byteOutput)) { + return objectOutput.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw ThrowingEx.asRuntime(e); + } + } + + /** Ensures that the lazy state has been evaluated. */ + public static void unlazy(Object in) { + if (in instanceof LazyForwardingEquality) { + ((LazyForwardingEquality) in).state(); + } else if (in instanceof DelegateFormatterStep) { + unlazy(((DelegateFormatterStep) in).delegateStep); + } else if (in instanceof Iterable) { + Iterable cast = (Iterable) in; + for (Object c : cast) { + unlazy(c); + } + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/LineEnding.java b/lib/src/main/java/com/diffplug/spotless/LineEnding.java index 6ea5372807..5b44b81889 100644 --- a/lib/src/main/java/com/diffplug/spotless/LineEnding.java +++ b/lib/src/main/java/com/diffplug/spotless/LineEnding.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package com.diffplug.spotless; import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; import java.io.Serializable; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Supplier; -import javax.annotation.Nullable; - /** * Represents the line endings which should be written by the tool. */ @@ -37,37 +38,47 @@ public Policy createPolicy() { return super.createPolicy(); } }, + /** Uses the same line endings as Git, and assumes that every single file being formatted will have the same line ending. */ + GIT_ATTRIBUTES_FAST_ALLSAME { + /** .gitattributes is path-specific, so you must use {@link LineEnding#createPolicy(File, Supplier)}. */ + @Override @Deprecated + public Policy createPolicy() { + return super.createPolicy(); + } + }, /** {@code \n} on unix systems, {@code \r\n} on windows systems. */ PLATFORM_NATIVE, /** {@code \r\n} */ WINDOWS, - /** {@code \n} */ - UNIX; + /** {@code \n} */ + UNIX, + /** {@code \r} */ + MAC_CLASSIC, + /** preserve the line ending of the first line (no matter which format) */ + PRESERVE; // @formatter:on /** Returns a {@link Policy} appropriate for files which are contained within the given rootFolder. */ public Policy createPolicy(File projectDir, Supplier> toFormat) { Objects.requireNonNull(projectDir, "projectDir"); Objects.requireNonNull(toFormat, "toFormat"); - if (this != GIT_ATTRIBUTES) { - return createPolicy(); + String gitAttributesMethod; + if (this == GIT_ATTRIBUTES) { + gitAttributesMethod = "create"; + } else if (this == GIT_ATTRIBUTES_FAST_ALLSAME) { + gitAttributesMethod = "createFastAllSame"; } else { - if (gitAttributesPolicyCreator == null) { - try { - Class clazz = Class.forName("com.diffplug.spotless.extra.GitAttributesLineEndings"); - Method method = clazz.getMethod("create", File.class, Supplier.class); - gitAttributesPolicyCreator = (proj, target) -> ThrowingEx.get(() -> (Policy) method.invoke(null, proj, target)); - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { - throw new IllegalStateException("LineEnding.GIT_ATTRIBUTES requires the spotless-lib-extra library, but it is not on the classpath", e); - } - } - // gitAttributesPolicyCreator will always be nonnull at this point - return gitAttributesPolicyCreator.apply(projectDir, toFormat); + return createPolicy(); + } + try { + Class clazz = Class.forName("com.diffplug.spotless.extra.GitAttributesLineEndings"); + Method method = clazz.getMethod(gitAttributesMethod, File.class, Supplier.class); + return ThrowingEx.get(() -> (Policy) method.invoke(null, projectDir, toFormat)); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + throw new IllegalStateException("LineEnding.GIT_ATTRIBUTES requires the spotless-lib-extra library, but it is not on the classpath", e); } } - private static volatile @Nullable BiFunction>, Policy> gitAttributesPolicyCreator; - // @formatter:off /** Should use {@link #createPolicy(File, Supplier)} instead, but this will work iff its a path-independent LineEnding policy. */ public Policy createPolicy() { @@ -75,6 +86,8 @@ public Policy createPolicy() { case PLATFORM_NATIVE: return _platformNativePolicy; case WINDOWS: return WINDOWS_POLICY; case UNIX: return UNIX_POLICY; + case MAC_CLASSIC: return MAC_CLASSIC_POLICY; + case PRESERVE: return PRESERVE_POLICY; default: throw new UnsupportedOperationException(this + " is a path-specific line ending."); } } @@ -94,8 +107,50 @@ public String getEndingFor(File file) { } } + static class PreserveLineEndingPolicy extends NoLambda.EqualityBasedOnSerialization implements Policy { + private static final long serialVersionUID = 2L; + + @Override + public String getEndingFor(File file) { + // assume US-ASCII encoding (only line ending characters need to be decoded anyways) + try (Reader reader = new FileReader(file, StandardCharsets.US_ASCII)) { + return getEndingFor(reader); + } catch (IOException e) { + throw new IllegalArgumentException("Could not determine line ending of file: " + file, e); + } + } + + static String getEndingFor(Reader reader) throws IOException { + char previousCharacter = 0; + char currentCharacter = 0; + int readResult; + while ((readResult = reader.read()) != -1) { + currentCharacter = (char)readResult; + if (currentCharacter == '\n') { + if (previousCharacter == '\r') { + return WINDOWS.str(); + } else { + return UNIX.str(); + } + } else { + if (previousCharacter == '\r') { + return MAC_CLASSIC.str(); + } + } + previousCharacter = currentCharacter; + } + if (previousCharacter == '\r') { + return MAC_CLASSIC.str(); + } + // assume UNIX line endings if no line ending was found + return UNIX.str(); + } + } + private static final Policy WINDOWS_POLICY = new ConstantLineEndingPolicy(WINDOWS.str()); private static final Policy UNIX_POLICY = new ConstantLineEndingPolicy(UNIX.str()); + private static final Policy MAC_CLASSIC_POLICY = new ConstantLineEndingPolicy(MAC_CLASSIC.str()); + private static final Policy PRESERVE_POLICY = new PreserveLineEndingPolicy(); private static final String _platformNative = System.getProperty("line.separator"); private static final Policy _platformNativePolicy = new ConstantLineEndingPolicy(_platformNative); private static final boolean nativeIsWin = _platformNative.equals(WINDOWS.str()); @@ -117,6 +172,7 @@ public String str() { case PLATFORM_NATIVE: return _platformNative; case WINDOWS: return "\r\n"; case UNIX: return "\n"; + case MAC_CLASSIC: return "\r"; default: throw new UnsupportedOperationException(this + " is a path-specific line ending."); } } @@ -137,12 +193,17 @@ public default boolean isUnix(File file) { /** Returns a string with exclusively unix line endings. */ public static String toUnix(String input) { - int firstNewline = input.lastIndexOf('\n'); - if (firstNewline == -1) { - // fastest way to detect if a string is already unix-only + int lastCarriageReturn = input.lastIndexOf('\r'); + if (lastCarriageReturn == -1) { return input; } else { - return input.replace("\r", ""); + if (input.lastIndexOf("\r\n") == -1) { + // it is MAC_CLASSIC \r + return input.replace('\r', '\n'); + } else { + // it is WINDOWS \r\n + return input.replace("\r", ""); + } } } } diff --git a/lib/src/main/java/com/diffplug/spotless/Lint.java b/lib/src/main/java/com/diffplug/spotless/Lint.java new file mode 100644 index 0000000000..b48a7bcbd2 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/Lint.java @@ -0,0 +1,216 @@ +/* + * Copyright 2022-2024 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 java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Models a linted line or line range. Note that there is no concept of severity level - responsibility + * for severity and confidence are pushed down to the configuration of the lint tool. If a lint makes it + * to Spotless, then it is by definition. + */ +public final class Lint implements Serializable { + public static Lint atUndefinedLine(String ruleId, String detail) { + return new Lint(LINE_UNDEFINED, ruleId, detail); + } + + public static Lint atLine(int line, String ruleId, String detail) { + return new Lint(line, ruleId, detail); + } + + public static Lint atLineRange(int lineStart, int lineEnd, String shortCode, String detail) { + return new Lint(lineStart, lineEnd, shortCode, detail); + } + + private static final long serialVersionUID = 1L; + + private int lineStart, lineEnd; // 1-indexed, inclusive + private String shortCode; // e.g. CN_IDIOM https://spotbugs.readthedocs.io/en/stable/bugDescriptions.html#cn-class-implements-cloneable-but-does-not-define-or-use-clone-method-cn-idiom + private String detail; + + private Lint(int lineStart, int lineEnd, String shortCode, String detail) { + if (lineEnd < lineStart) { + throw new IllegalArgumentException("lineEnd must be >= lineStart: lineStart=" + lineStart + " lineEnd=" + lineEnd); + } + this.lineStart = lineStart; + this.lineEnd = lineEnd; + this.shortCode = LineEnding.toUnix(shortCode); + this.detail = LineEnding.toUnix(detail); + } + + private Lint(int line, String shortCode, String detail) { + this(line, line, shortCode, detail); + } + + public int getLineStart() { + return lineStart; + } + + public int getLineEnd() { + return lineEnd; + } + + public String getShortCode() { + return shortCode; + } + + public String getDetail() { + return detail; + } + + /** Any exception which implements this interface will have its lints extracted and reported cleanly to the user. */ + public interface Has { + List getLints(); + } + + /** An exception for shortcutting execution to report a lint to the user. */ + static class ShortcutException extends RuntimeException implements Has { + public ShortcutException(Lint... lints) { + this(Arrays.asList(lints)); + } + + private final List lints; + + ShortcutException(Collection lints) { + super(lints.iterator().next().detail); + this.lints = List.copyOf(lints); + } + + @Override + public List getLints() { + return lints; + } + } + + /** Returns an exception which will wrap all of the given lints using {@link Has} */ + public static RuntimeException shortcut(Collection lints) { + return new ShortcutException(lints); + } + + /** Returns an exception which will wrap this lint using {@link Has} */ + public RuntimeException shortcut() { + return new ShortcutException(this); + } + + @Override + public String toString() { + if (lineStart == lineEnd) { + if (lineStart == LINE_UNDEFINED) { + return "LINE_UNDEFINED: (" + shortCode + ") " + detail; + } else { + return "L" + lineStart + ": (" + shortCode + ") " + detail; + } + } else { + return "L" + lineStart + "-" + lineEnd + ": (" + shortCode + ") " + detail; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Lint lint = (Lint) o; + return lineStart == lint.lineStart && lineEnd == lint.lineEnd && Objects.equals(shortCode, lint.shortCode) && Objects.equals(detail, lint.detail); + } + + @Override + public int hashCode() { + return Objects.hash(lineStart, lineEnd, shortCode, detail); + } + + /** Attempts to parse a line number from the given exception. */ + static Lint createFromThrowable(FormatterStep step, Throwable e) { + Throwable current = e; + while (current != null) { + String message = current.getMessage(); + int lineNumber = lineNumberFor(message); + if (lineNumber != -1) { + return new Lint(lineNumber, step.getName(), msgFrom(message)); + } + current = current.getCause(); + } + String exceptionName = e.getClass().getName(); + String detail = ThrowingEx.stacktrace(e); + if (detail.startsWith(exceptionName + ": ")) { + detail = detail.substring(exceptionName.length() + 2); + } + Matcher matcher = Pattern.compile("line (\\d+)").matcher(detail); + int line = LINE_UNDEFINED; + if (matcher.find()) { + line = Integer.parseInt(matcher.group(1)); + } + return Lint.atLine(line, exceptionName, detail); + } + + private static int lineNumberFor(String message) { + if (message == null) { + return -1; + } + int firstColon = message.indexOf(':'); + if (firstColon == -1) { + return -1; + } + String candidateNum = message.substring(0, firstColon); + try { + return Integer.parseInt(candidateNum); + } catch (NumberFormatException e) { + return -1; + } + } + + private static String msgFrom(String message) { + for (int i = 0; i < message.length(); ++i) { + if (Character.isLetter(message.charAt(i))) { + return message.substring(i); + } + } + return ""; + } + + public static final int LINE_UNDEFINED = -1; + + public void addWarningMessageTo(StringBuilder buffer, String stepName, boolean oneLine) { + if (lineStart == Lint.LINE_UNDEFINED) { + buffer.append("LINE_UNDEFINED"); + } else { + buffer.append("L"); + buffer.append(lineStart); + if (lineEnd != lineStart) { + buffer.append("-").append(lineEnd); + } + } + buffer.append(" "); + buffer.append(stepName).append("(").append(shortCode).append(") "); + + int firstNewline = detail.indexOf('\n'); + if (firstNewline == -1) { + buffer.append(detail); + } else if (oneLine) { + buffer.append(detail, 0, firstNewline); + buffer.append(" (...)"); + } else { + buffer.append(detail); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/LintState.java b/lib/src/main/java/com/diffplug/spotless/LintState.java new file mode 100644 index 0000000000..3068872256 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/LintState.java @@ -0,0 +1,214 @@ +/* + * Copyright 2024 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 java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; + +import javax.annotation.Nullable; + +public class LintState { + private final DirtyState dirtyState; + private final @Nullable List> lintsPerStep; + + LintState(DirtyState dirtyState, @Nullable List> lintsPerStep) { + this.dirtyState = dirtyState; + this.lintsPerStep = lintsPerStep; + } + + public DirtyState getDirtyState() { + return dirtyState; + } + + public boolean isHasLints() { + return lintsPerStep != null; + } + + public boolean isClean() { + return dirtyState.isClean() && !isHasLints(); + } + + public LinkedHashMap> getLintsByStep(Formatter formatter) { + if (lintsPerStep == null) { + throw new IllegalStateException("Check `isHasLints` first!"); + } + if (lintsPerStep.size() != formatter.getSteps().size()) { + throw new IllegalStateException("LintState was created with a different formatter!"); + } + LinkedHashMap> result = new LinkedHashMap<>(); + for (int i = 0; i < lintsPerStep.size(); i++) { + List lints = lintsPerStep.get(i); + if (lints != null) { + FormatterStep step = formatter.getSteps().get(i); + result.put(step.getName(), lints); + } + } + return result; + } + + public LintState withRemovedSuppressions(Formatter formatter, String relativePath, List suppressions) { + if (lintsPerStep == null) { + return this; + } + if (formatter.getSteps().size() != lintsPerStep.size()) { + throw new IllegalStateException("LintState was created with a different formatter!"); + } + boolean changed = false; + ValuePerStep> perStepFiltered = new ValuePerStep<>(formatter); + for (int i = 0; i < lintsPerStep.size(); i++) { + FormatterStep step = formatter.getSteps().get(i); + List lintsOriginal = lintsPerStep.get(i); + if (lintsOriginal != null) { + List lints = new ArrayList<>(lintsOriginal); + Iterator iter = lints.iterator(); + while (iter.hasNext()) { + Lint lint = iter.next(); + for (LintSuppression suppression : suppressions) { + if (suppression.suppresses(relativePath, step, lint)) { + changed = true; + iter.remove(); + break; + } + } + } + if (!lints.isEmpty()) { + perStepFiltered.set(i, lints); + } + } + } + if (changed) { + return new LintState(dirtyState, perStepFiltered.indexOfFirstValue() == -1 ? null : perStepFiltered); + } else { + return this; + } + } + + public String asStringDetailed(File file, Formatter formatter) { + return asString(file, formatter, false); + } + + public String asStringOneLine(File file, Formatter formatter) { + return asString(file, formatter, true); + } + + private String asString(File file, Formatter formatter, boolean oneLine) { + if (!isHasLints()) { + return "(none)"; + } else { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < lintsPerStep.size(); i++) { + List lints = lintsPerStep.get(i); + if (lints != null) { + FormatterStep step = formatter.getSteps().get(i); + for (Lint lint : lints) { + result.append(file.getName()).append(":"); + lint.addWarningMessageTo(result, step.getName(), oneLine); + result.append("\n"); + } + } + } + result.setLength(result.length() - 1); + return result.toString(); + } + } + + public static LintState of(Formatter formatter, File file) throws IOException { + return of(formatter, file, Files.readAllBytes(file.toPath())); + } + + public static LintState of(Formatter formatter, File file, byte[] rawBytes) { + var exceptions = new ValuePerStep(formatter); + var raw = new String(rawBytes, formatter.getEncoding()); + var dirty = DirtyState.of(formatter, file, rawBytes, raw, exceptions); + + String toLint = LineEnding.toUnix(dirty.isClean() || dirty.didNotConverge() ? raw : new String(dirty.canonicalBytes(), formatter.getEncoding())); + + var lints = new ValuePerStep>(formatter); + // if a step did not throw an exception, then it gets to check for lints if it wants + for (int i = 0; i < formatter.getSteps().size(); i++) { + FormatterStep step = formatter.getSteps().get(i); + Throwable exception = exceptions.get(i); + if (exception == null || exception == formatStepCausedNoChange()) { + try { + var lintsForStep = step.lint(toLint, file); + if (lintsForStep != null && !lintsForStep.isEmpty()) { + lints.set(i, lintsForStep); + } + } catch (Exception e) { + lints.set(i, List.of(Lint.createFromThrowable(step, e))); + } + } + } + // for steps that did throw an exception, we will turn those into lints + // we try to reuse the exception if possible, but that is only possible if other steps + // didn't change the formatted value. so we start at the end, and note when the string + // gets changed by a step. if it does, we rerun the steps to get an exception with accurate line numbers. + boolean nothingHasChangedSinceLast = true; + for (int i = formatter.getSteps().size() - 1; i >= 0; i--) { + FormatterStep step = formatter.getSteps().get(i); + Throwable exception = exceptions.get(i); + if (exception != null && exception != formatStepCausedNoChange()) { + nothingHasChangedSinceLast = false; + } + Throwable exceptionForLint; + if (nothingHasChangedSinceLast) { + exceptionForLint = exceptions.get(i); + } else { + // steps changed the content, so we need to rerun to get an exception with accurate line numbers + try { + step.format(toLint, file); + exceptionForLint = null; // the exception "went away" because it got fixed by a later step + } catch (Throwable e) { + exceptionForLint = e; + } + } + List lintsForStep; + if (exceptionForLint instanceof Lint.Has) { + lintsForStep = ((Lint.Has) exceptionForLint).getLints(); + } else if (exceptionForLint != null && exceptionForLint != formatStepCausedNoChange()) { + lintsForStep = List.of(Lint.createFromThrowable(step, exceptionForLint)); + } else { + lintsForStep = List.of(); + } + if (!lintsForStep.isEmpty()) { + lints.set(i, lintsForStep); + } + } + return new LintState(dirty, lints.indexOfFirstValue() == -1 ? null : lints); + } + + /** Returns the DirtyState which corresponds to {@code isClean()}. */ + public static LintState clean() { + return isClean; + } + + private static final LintState isClean = new LintState(DirtyState.clean(), null); + + static Throwable formatStepCausedNoChange() { + return FormatterCausedNoChange.INSTANCE; + } + + private static class FormatterCausedNoChange extends Exception { + private static final long serialVersionUID = 1L; + + static final FormatterCausedNoChange INSTANCE = new FormatterCausedNoChange(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/LintSuppression.java b/lib/src/main/java/com/diffplug/spotless/LintSuppression.java new file mode 100644 index 0000000000..d84107bd3a --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/LintSuppression.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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 java.util.Objects; + +public class LintSuppression implements java.io.Serializable { + private static final long serialVersionUID = 1L; + + private static final String ALL = "*"; + private String path = ALL; + private String step = ALL; + private String shortCode = ALL; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = Objects.requireNonNull(path); + } + + public String getStep() { + return step; + } + + public void setStep(String step) { + this.step = Objects.requireNonNull(step); + } + + public String getShortCode() { + return shortCode; + } + + public void setShortCode(String shortCode) { + this.shortCode = Objects.requireNonNull(shortCode); + } + + public boolean suppresses(String relativePath, FormatterStep formatterStep, Lint lint) { + if (path.equals(ALL) || path.equals(relativePath)) { + if (step.equals(ALL) || formatterStep.getName().equals(this.step)) { + if (shortCode.equals(ALL) || lint.getShortCode().equals(this.shortCode)) { + return true; + } + } + } + return false; + } + + public void ensureDoesNotSuppressAll() { + boolean suppressAll = path.equals(ALL) && step.equals(ALL) && shortCode.equals(ALL); + if (suppressAll) { + throw new IllegalArgumentException("You must specify a specific `file`, `step`, or `shortCode`."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LintSuppression that = (LintSuppression) o; + return Objects.equals(path, that.path) && Objects.equals(step, that.step) && Objects.equals(shortCode, that.shortCode); + } + + @Override + public int hashCode() { + return Objects.hash(path, step, shortCode); + } + + @Override + public String toString() { + return "LintSuppression{" + + "file='" + path + '\'' + + ", step='" + step + '\'' + + ", code='" + shortCode + '\'' + + '}'; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/NoLambda.java b/lib/src/main/java/com/diffplug/spotless/NoLambda.java index ce326a6ceb..d2ca6c1401 100644 --- a/lib/src/main/java/com/diffplug/spotless/NoLambda.java +++ b/lib/src/main/java/com/diffplug/spotless/NoLambda.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ /** * Marker interface to prevent lambda implementations of * single-method interfaces that require serializability. - * + *

* In order for Spotless to support up-to-date checks, all * of its parameters must be {@link Serializable} so that * entries can be written to file, and they must implement * equals and hashCode correctly. - * + *

* This interface and its standard implementation, * {@link EqualityBasedOnSerialization}, are a quick way * to accomplish these goals. @@ -34,7 +34,7 @@ public interface NoLambda extends Serializable { /** * Returns a byte array representation of everything inside this {@code SerializableFileFilter}. - * + *

* The main purpose of this method is to ensure one can't instantiate this class with lambda * expressions, which are notoriously difficult to serialize and deserialize properly. (See * {@code SerializableFileFilterImpl.SkipFilesNamed} for an example of how to make a serializable diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/package-info.java b/lib/src/main/java/com/diffplug/spotless/OnMatch.java similarity index 78% rename from _ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/package-info.java rename to lib/src/main/java/com/diffplug/spotless/OnMatch.java index da74c0471e..098fb28f13 100644 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/osgi/package-info.java +++ b/lib/src/main/java/com/diffplug/spotless/OnMatch.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,5 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Simplified OSGIi implementation bypassing Equinox module layer. */ -package com.diffplug.spotless.extra.eclipse.base.osgi; +package com.diffplug.spotless; + +/** Enum to make boolean logic more readable. */ +public enum OnMatch { + INCLUDE, EXCLUDE +} diff --git a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java index 59ef242f6c..5ec7ef1cad 100644 --- a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java +++ b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,23 +18,18 @@ import static com.diffplug.spotless.LibPreconditions.requireElementsNonNull; import java.io.File; -import java.io.IOException; -import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.Function; -import javax.annotation.Nullable; - /** * Models the result of applying a {@link Formatter} on a given {@link File} * while characterizing various failure modes (slow convergence, cycles, and divergence). - * + *

* See {@link #check(Formatter, File)} as the entry point to this class. */ public final class PaddedCell { @@ -78,9 +73,9 @@ public List steps() { /** * Applies the given formatter to the given file, checking that * F(F(input)) == F(input). - * + *

* If it meets this test, {@link #misbehaved()} will return false. - * + *

* If it fails the test, {@link #misbehaved()} will return true, and you can find * out more about the misbehavior based on its {@link Type}. * @@ -91,29 +86,29 @@ public static PaddedCell check(Formatter formatter, File file) { byte[] rawBytes = ThrowingEx.get(() -> Files.readAllBytes(file.toPath())); String raw = new String(rawBytes, formatter.getEncoding()); String original = LineEnding.toUnix(raw); - return check(formatter, file, original, MAX_CYCLE); + return check(formatter, file, original, MAX_CYCLE, new ValuePerStep<>(formatter)); } public static PaddedCell check(Formatter formatter, File file, String originalUnix) { - return check( - Objects.requireNonNull(formatter, "formatter"), - Objects.requireNonNull(file, "file"), - Objects.requireNonNull(originalUnix, "originalUnix"), - MAX_CYCLE); + return check(formatter, file, originalUnix, new ValuePerStep<>(formatter)); + } + + public static PaddedCell check(Formatter formatter, File file, String originalUnix, ValuePerStep exceptionPerStep) { + return check(formatter, file, originalUnix, MAX_CYCLE, exceptionPerStep); } private static final int MAX_CYCLE = 10; - private static PaddedCell check(Formatter formatter, File file, String original, int maxLength) { + private static PaddedCell check(Formatter formatter, File file, String original, int maxLength, ValuePerStep exceptionPerStep) { if (maxLength < 2) { throw new IllegalArgumentException("maxLength must be at least 2"); } - String appliedOnce = formatter.compute(original, file); + String appliedOnce = formatter.computeWithLint(original, file, exceptionPerStep); if (appliedOnce.equals(original)) { return Type.CONVERGE.create(file, Collections.singletonList(appliedOnce)); } - String appliedTwice = formatter.compute(appliedOnce, file); + String appliedTwice = formatter.computeWithLint(appliedOnce, file, exceptionPerStep); if (appliedOnce.equals(appliedTwice)) { return Type.CONVERGE.create(file, Collections.singletonList(appliedOnce)); } @@ -123,7 +118,7 @@ private static PaddedCell check(Formatter formatter, File file, String original, appliedN.add(appliedTwice); String input = appliedTwice; while (appliedN.size() < maxLength) { - String output = formatter.compute(input, file); + String output = formatter.computeWithLint(input, file, exceptionPerStep); if (output.equals(input)) { return Type.CONVERGE.create(file, appliedN); } else { @@ -176,108 +171,4 @@ public String userMessage() { } // @formatter:on } - - /** - * Calculates whether the given file is dirty according to a PaddedCell invocation of the given formatter. - * DirtyState includes the clean state of the file, as well as a warning if we were not able to apply the formatter - * due to diverging idempotence. - */ - public static DirtyState calculateDirtyState(Formatter formatter, File file) throws IOException { - Objects.requireNonNull(formatter, "formatter"); - Objects.requireNonNull(file, "file"); - - byte[] rawBytes = Files.readAllBytes(file.toPath()); - return calculateDirtyState(formatter, file, rawBytes); - } - - public static DirtyState calculateDirtyState(Formatter formatter, File file, byte[] rawBytes) throws IOException { - String raw = new String(rawBytes, formatter.getEncoding()); - // check that all characters were encodable - String encodingError = EncodingErrorMsg.msg(raw, rawBytes, formatter.getEncoding()); - if (encodingError != null) { - throw new IllegalArgumentException(encodingError); - } - String rawUnix = LineEnding.toUnix(raw); - - // enforce the format - String formattedUnix = formatter.compute(rawUnix, file); - // convert the line endings if necessary - String formatted = formatter.computeLineEndings(formattedUnix, file); - - // if F(input) == input, then the formatter is well-behaving and the input is clean - byte[] formattedBytes = formatted.getBytes(formatter.getEncoding()); - if (Arrays.equals(rawBytes, formattedBytes)) { - return isClean; - } - - // F(input) != input, so we'll do a padded check - String doubleFormattedUnix = formatter.compute(formattedUnix, file); - if (doubleFormattedUnix.equals(formattedUnix)) { - // most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case - return new DirtyState(formattedBytes); - } - - PaddedCell cell = PaddedCell.check(formatter, file, rawUnix); - if (!cell.isResolvable()) { - return didNotConverge; - } - - // get the canonical bytes - String canonicalUnix = cell.canonical(); - String canonical = formatter.computeLineEndings(canonicalUnix, file); - byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding()); - if (!Arrays.equals(rawBytes, canonicalBytes)) { - // and write them to disk if needed - return new DirtyState(canonicalBytes); - } else { - return isClean; - } - } - - /** - * The clean/dirty state of a single file. Intended use: - * - {@link #isClean()} means that the file is is clean, and there's nothing else to say - * - {@link #didNotConverge()} means that we were unable to determine a clean state - * - once you've tested the above conditions and you know that it's a dirty file with a converged state, - * then you can call {@link #writeCanonicalTo(OutputStream)} to get the canonical form of the given file. - */ - public static class DirtyState { - @Nullable - private final byte[] canonicalBytes; - - private DirtyState(@Nullable byte[] canonicalBytes) { - this.canonicalBytes = canonicalBytes; - } - - public boolean isClean() { - return this == isClean; - } - - public boolean didNotConverge() { - return this == didNotConverge; - } - - private byte[] canonicalBytes() { - if (canonicalBytes == null) { - throw new IllegalStateException("First make sure that {@code !isClean()} and {@code !didNotConverge()}"); - } - return canonicalBytes; - } - - public void writeCanonicalTo(File file) throws IOException { - Files.write(file.toPath(), canonicalBytes()); - } - - public void writeCanonicalTo(OutputStream out) throws IOException { - out.write(canonicalBytes()); - } - } - - /** Returns the DirtyState which corresponds to {@code isClean()}. */ - public static DirtyState isClean() { - return isClean; - } - - private static final DirtyState didNotConverge = new DirtyState(null); - private static final DirtyState isClean = new DirtyState(null); } diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index c798ed6c07..e7755f75bd 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,26 +15,35 @@ */ package com.diffplug.spotless; +import static java.util.Objects.requireNonNull; + import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Shelling out to a process is harder than it ought to be in Java. * If you don't read stdout and stderr on their own threads, you risk * deadlock on a clogged buffer. - * + *

* ProcessRunner allocates two threads specifically for the purpose of * flushing stdout and stderr to buffers. These threads will remain alive until * the ProcessRunner is closed, so it is especially useful for repeated @@ -43,10 +52,21 @@ public class ProcessRunner implements AutoCloseable { private final ExecutorService threadStdOut = Executors.newSingleThreadExecutor(); private final ExecutorService threadStdErr = Executors.newSingleThreadExecutor(); - private final ByteArrayOutputStream bufStdOut = new ByteArrayOutputStream(); - private final ByteArrayOutputStream bufStdErr = new ByteArrayOutputStream(); + private final ByteArrayOutputStream bufStdOut; + private final ByteArrayOutputStream bufStdErr; - public ProcessRunner() {} + public ProcessRunner() { + this(-1); + } + + public static ProcessRunner usingRingBuffersOfCapacity(int limit) { + return new ProcessRunner(limit); + } + + private ProcessRunner(int limitedBuffers) { + this.bufStdOut = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream(); + this.bufStdErr = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream(); + } /** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */ public Result shell(String cmd) throws IOException, InterruptedException { @@ -55,13 +75,18 @@ public Result shell(String cmd) throws IOException, InterruptedException { /** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */ public Result shellWinUnix(String cmdWin, String cmdUnix) throws IOException, InterruptedException { + return shellWinUnix(null, null, cmdWin, cmdUnix); + } + + /** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */ + public Result shellWinUnix(@Nullable File cwd, @Nullable Map environment, String cmdWin, String cmdUnix) throws IOException, InterruptedException { List args; if (FileSignature.machineIsWin()) { args = Arrays.asList("cmd", "/c", cmdWin); } else { args = Arrays.asList("sh", "-c", cmdUnix); } - return exec(args); + return exec(cwd, environment, null, args); } /** Creates a process with the given arguments. */ @@ -70,32 +95,77 @@ public Result exec(String... args) throws IOException, InterruptedException { } /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */ - public Result exec(byte[] stdin, String... args) throws IOException, InterruptedException { + public Result exec(@Nullable byte[] stdin, String... args) throws IOException, InterruptedException { return exec(stdin, Arrays.asList(args)); } /** Creates a process with the given arguments. */ public Result exec(List args) throws IOException, InterruptedException { - return exec(new byte[0], args); + return exec(null, args); } /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */ - public Result exec(byte[] stdin, List args) throws IOException, InterruptedException { + public Result exec(@Nullable byte[] stdin, List args) throws IOException, InterruptedException { + return exec(null, null, stdin, args); + } + + /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */ + public Result exec(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, List args) throws IOException, InterruptedException { + LongRunningProcess process = start(cwd, environment, stdin, args); + try { + // wait for the process to finish + process.waitFor(); + // collect the output + return process.result(); + } catch (ExecutionException e) { + throw ThrowingEx.asRuntime(e); + } + } + + /** + * Creates a process with the given arguments, the given byte array is written to stdin immediately. + *
+ * Delegates to {@link #start(File, Map, byte[], boolean, List)} with {@code false} for {@code redirectErrorStream}. + */ + public LongRunningProcess start(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, List args) throws IOException { + return start(cwd, environment, stdin, false, args); + } + + /** + * Creates a process with the given arguments, the given byte array is written to stdin immediately. + *
+ * The process is not waited for, so the caller is responsible for calling {@link LongRunningProcess#waitFor()} (if needed). + *
+ * To dispose this {@code ProcessRunner} instance, either call {@link #close()} or {@link LongRunningProcess#close()}. After + * {@link #close()} or {@link LongRunningProcess#close()} has been called, this {@code ProcessRunner} instance must not be used anymore. + */ + public LongRunningProcess start(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, boolean redirectErrorStream, List args) throws IOException { + checkState(); ProcessBuilder builder = new ProcessBuilder(args); + if (cwd != null) { + builder.directory(cwd); + } + if (environment != null) { + builder.environment().putAll(environment); + } + if (stdin == null) { + stdin = new byte[0]; + } + if (redirectErrorStream) { + builder.redirectErrorStream(true); + } + Process process = builder.start(); Future outputFut = threadStdOut.submit(() -> drainToBytes(process.getInputStream(), bufStdOut)); - Future errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr)); + Future errorFut = null; + if (!redirectErrorStream) { + errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr)); + } // write stdin process.getOutputStream().write(stdin); + process.getOutputStream().flush(); process.getOutputStream().close(); - // wait for the process to finish - int exitCode = process.waitFor(); - try { - // collect the output - return new Result(args, exitCode, outputFut.get(), errorFut.get()); - } catch (ExecutionException e) { - throw ThrowingEx.asRuntime(e); - } + return new LongRunningProcess(process, args, outputFut, errorFut); } private static void drain(InputStream input, OutputStream output) throws IOException { @@ -118,17 +188,24 @@ public void close() { threadStdErr.shutdown(); } + /** Checks if this {@code ProcessRunner} instance is still usable. */ + private void checkState() { + if (threadStdOut.isShutdown() || threadStdErr.isShutdown()) { + throw new IllegalStateException("ProcessRunner has been closed and must not be used anymore."); + } + } + @SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) public static class Result { private final List args; private final int exitCode; private final byte[] stdOut, stdErr; - public Result(List args, int exitCode, byte[] stdOut, byte[] stdErr) { + public Result(@Nonnull List args, int exitCode, @Nonnull byte[] stdOut, @Nullable byte[] stdErr) { this.args = args; this.exitCode = exitCode; this.stdOut = stdOut; - this.stdErr = stdErr; + this.stdErr = (stdErr == null ? new byte[0] : stdErr); } public List args() { @@ -147,6 +224,14 @@ public byte[] stdErr() { return stdErr; } + public String stdOutUtf8() { + return new String(stdOut, StandardCharsets.UTF_8); + } + + public String stdErrUtf8() { + return new String(stdErr, StandardCharsets.UTF_8); + } + /** Returns true if the exit code was not zero. */ public boolean exitNotZero() { return exitCode != 0; @@ -155,7 +240,7 @@ public boolean exitNotZero() { /** * Asserts that the exit code was zero, and if so, returns * the content of stdout encoded with the given charset. - * + *

* If the exit code was not zero, throws an exception * with useful debugging information. */ @@ -191,8 +276,86 @@ public String toString() { } }; perStream.accept(" stdout", stdOut); - perStream.accept(" stderr", stdErr); + if (stdErr.length > 0) { + perStream.accept(" stderr", stdErr); + } return builder.toString(); } } + + /** + * A long-running process that can be waited for. + */ + public class LongRunningProcess extends Process implements AutoCloseable { + + private final Process delegate; + private final List args; + private final Future outputFut; + private final Future errorFut; + + public LongRunningProcess(@Nonnull Process delegate, @Nonnull List args, @Nonnull Future outputFut, @Nullable Future errorFut) { + this.delegate = requireNonNull(delegate); + this.args = args; + this.outputFut = outputFut; + this.errorFut = errorFut; + } + + @Override + public OutputStream getOutputStream() { + return delegate.getOutputStream(); + } + + @Override + public InputStream getInputStream() { + return delegate.getInputStream(); + } + + @Override + public InputStream getErrorStream() { + return delegate.getErrorStream(); + } + + @Override + public int waitFor() throws InterruptedException { + return delegate.waitFor(); + } + + @Override + public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.waitFor(timeout, unit); + } + + @Override + public int exitValue() { + return delegate.exitValue(); + } + + @Override + public void destroy() { + delegate.destroy(); + } + + @Override + public Process destroyForcibly() { + return delegate.destroyForcibly(); + } + + @Override + public boolean isAlive() { + return delegate.isAlive(); + } + + public Result result() throws ExecutionException, InterruptedException { + int exitCode = waitFor(); + return new Result(args, exitCode, this.outputFut.get(), (this.errorFut != null ? this.errorFut.get() : null)); + } + + @Override + public void close() { + if (isAlive()) { + destroy(); + } + ProcessRunner.this.close(); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java b/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java new file mode 100644 index 0000000000..da4fc6aa04 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +class RingBufferByteArrayOutputStream extends ByteArrayOutputStream { + + private final int limit; + + private int zeroIndexPointer = 0; + + private boolean isOverLimit = false; + + public RingBufferByteArrayOutputStream(int limit) { + this(limit, 32); + } + + public RingBufferByteArrayOutputStream(int limit, int initialCapacity) { + super(initialCapacity); + if (limit < initialCapacity) { + throw new IllegalArgumentException("Limit must be greater than initial capacity. Limit: " + limit + ", initial capacity: " + initialCapacity); + } + if (limit < 2) { + throw new IllegalArgumentException("Limit must be greater than or equal to 2 but is " + limit); + } + if (limit % 2 != 0) { + throw new IllegalArgumentException("Limit must be an even number but is " + limit); // to fit 16 bit unicode chars + } + this.limit = limit; + } + + // ---- writing + @Override + public synchronized void write(int b) { + if (count < limit) { + super.write(b); + return; + } + isOverLimit = true; + buf[zeroIndexPointer] = (byte) b; + zeroIndexPointer = (zeroIndexPointer + 1) % limit; + } + + @Override + public synchronized void write(byte[] b, int off, int len) { + int remaining = limit - count; + if (remaining >= len) { + super.write(b, off, len); + return; + } + if (remaining > 0) { + // write what we can "normally" + super.write(b, off, remaining); + // rest delegated + write(b, off + remaining, len - remaining); + return; + } + // we are over the limit + isOverLimit = true; + // write till limit is reached + int writeTillLimit = Math.min(len, limit - zeroIndexPointer); + System.arraycopy(b, off, buf, zeroIndexPointer, writeTillLimit); + zeroIndexPointer = (zeroIndexPointer + writeTillLimit) % limit; + if (writeTillLimit < len) { + // write rest + write(b, off + writeTillLimit, len - writeTillLimit); + } + } + + @Override + public synchronized void reset() { + super.reset(); + zeroIndexPointer = 0; + isOverLimit = false; + } + + // ---- output + @Override + public synchronized void writeTo(OutputStream out) throws IOException { + if (!isOverLimit) { + super.writeTo(out); + return; + } + out.write(buf, zeroIndexPointer, limit - zeroIndexPointer); + out.write(buf, 0, zeroIndexPointer); + } + + @Override + public synchronized byte[] toByteArray() { + if (!isOverLimit) { + return super.toByteArray(); + } + byte[] result = new byte[limit]; + System.arraycopy(buf, zeroIndexPointer, result, 0, limit - zeroIndexPointer); + System.arraycopy(buf, 0, result, limit - zeroIndexPointer, zeroIndexPointer); + return result; + } + + @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We want to use the default encoding here since this is contract on ByteArrayOutputStream") + @Override + public synchronized String toString() { + if (!isOverLimit) { + return super.toString(); + } + return new String(buf, zeroIndexPointer, limit - zeroIndexPointer) + new String(buf, 0, zeroIndexPointer); + } + + @Override + public synchronized String toString(String charsetName) throws UnsupportedEncodingException { + if (!isOverLimit) { + return super.toString(charsetName); + } + return new String(buf, zeroIndexPointer, limit - zeroIndexPointer, charsetName) + new String(buf, 0, zeroIndexPointer, charsetName); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/SerializedFunction.java b/lib/src/main/java/com/diffplug/spotless/SerializedFunction.java new file mode 100644 index 0000000000..7cadb120d6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/SerializedFunction.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023-2024 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 java.io.Serializable; + +@FunctionalInterface +public interface SerializedFunction extends Serializable, ThrowingEx.Function { + static SerializedFunction identity() { + return t -> t; + } + + static SerializedFunction alwaysReturns(R value) { + return new AlwaysReturns(value); + } + + class AlwaysReturns implements SerializedFunction { + private static final long serialVersionUID = 1L; + private final R value; + + AlwaysReturns(R value) { + this.value = value; + } + + @Override + public R apply(T t) { + return value; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/SpotlessCache.java b/lib/src/main/java/com/diffplug/spotless/SpotlessCache.java index b90682f1b6..237e326482 100644 --- a/lib/src/main/java/com/diffplug/spotless/SpotlessCache.java +++ b/lib/src/main/java/com/diffplug/spotless/SpotlessCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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,9 @@ import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** @@ -34,6 +37,8 @@ * when Spotless is no longer in use to release any resources it has grabbed. */ public final class SpotlessCache { + private static final Logger LOGGER = LoggerFactory.getLogger(SpotlessCache.class); + /** Allows comparing keys based on their serialization. */ static final class SerializedKey { final byte[] serialized; @@ -68,7 +73,10 @@ synchronized ClassLoader classloader(JarState state) { synchronized ClassLoader classloader(Serializable key, JarState state) { SerializedKey serializedKey = new SerializedKey(key); return cache - .computeIfAbsent(serializedKey, k -> new FeatureClassLoader(state.jarUrls(), this.getClass().getClassLoader())); + .computeIfAbsent(serializedKey, k -> { + LOGGER.debug("Allocating an additional FeatureClassLoader for key={} Cache.size was {}", key, cache.size()); + return new FeatureClassLoader(state.jarUrls(), this.getClass().getClassLoader()); + }); } static SpotlessCache instance() { diff --git a/lib/src/main/java/com/diffplug/spotless/TestingOnly.java b/lib/src/main/java/com/diffplug/spotless/TestingOnly.java new file mode 100644 index 0000000000..89fb791789 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/TestingOnly.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 java.util.Locale; + +/** These FormatterStep are meant to be used for testing only. */ +public class TestingOnly { + public static FormatterStep lowercase() { + return FormatterStep.create("lowercase", "lowercaseStateUnused", unused -> TestingOnly::lowercase); + } + + private static String lowercase(String raw) { + return raw.toLowerCase(Locale.ROOT); + } + + public static FormatterStep diverge() { + return FormatterStep.create("diverge", "divergeStateUnused", unused -> TestingOnly::diverge); + } + + private static String diverge(String raw) { + return raw + " "; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/ThrowingEx.java b/lib/src/main/java/com/diffplug/spotless/ThrowingEx.java index f664c62bf3..7e017e0989 100644 --- a/lib/src/main/java/com/diffplug/spotless/ThrowingEx.java +++ b/lib/src/main/java/com/diffplug/spotless/ThrowingEx.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,13 @@ */ package com.diffplug.spotless; +import java.io.PrintWriter; +import java.io.StringWriter; + /** * Basic functional interfaces which throw exception, along with * static helper methods for calling them. - * + *

* Contains most of the functionality of Durian's Throwing and Errors * classes, but stripped down and renamed to avoid any confusion. */ @@ -80,7 +83,7 @@ public static java.util.function.Function wrap(ThrowingEx.Function< /** * Casts or wraps the given exception to be a RuntimeException. - * + *

* If the input exception is a RuntimeException, it is simply * cast and returned. Otherwise, it wrapped in a * {@link WrappedAsRuntimeException} and returned. @@ -142,4 +145,12 @@ public WrappedAsRuntimeException(Throwable e) { super(e); } } + + public static String stacktrace(Throwable e) { + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + e.printStackTrace(writer); + writer.flush(); + return out.toString(); + } } diff --git a/lib/src/main/java/com/diffplug/spotless/ValuePerStep.java b/lib/src/main/java/com/diffplug/spotless/ValuePerStep.java new file mode 100644 index 0000000000..314bdc7e60 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/ValuePerStep.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 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 java.util.AbstractList; + +import javax.annotation.Nullable; + +/** + * Fixed-size list which maintains a list of exceptions, one per step of the formatter. + * Usually this list will be empty or have only a single value, so it is optimized for stack allocation in those cases. + */ +class ValuePerStep extends AbstractList { + private final int size; + private @Nullable T value; + private int valueIdx; + private @Nullable Object[] multipleValues = null; + + ValuePerStep(Formatter formatter) { + this.size = formatter.getSteps().size(); + } + + @Override + public @Nullable T set(int index, T newValue) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + if (this.value == null) { + this.valueIdx = index; + this.value = newValue; + return null; + } else if (this.multipleValues != null) { + T previousValue = (T) multipleValues[index]; + multipleValues[index] = newValue; + return previousValue; + } else { + if (index == valueIdx) { + T previousValue = this.value; + this.value = newValue; + return previousValue; + } else { + multipleValues = new Object[size]; + multipleValues[valueIdx] = this.value; + multipleValues[index] = newValue; + return null; + } + } + } + + @Override + public T get(int index) { + if (multipleValues != null) { + return (T) multipleValues[index]; + } else if (valueIdx == index) { + return value; + } else { + return null; + } + } + + public int indexOfFirstValue() { + if (multipleValues != null) { + for (int i = 0; i < multipleValues.length; i++) { + if (multipleValues[i] != null) { + return i; + } + } + return -1; + } else if (value != null) { + return valueIdx; + } else { + return -1; + } + } + + @Override + public int size() { + return size; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/annotations/Internal.java b/lib/src/main/java/com/diffplug/spotless/annotations/Internal.java index 77a318f268..d87265fe2d 100644 --- a/lib/src/main/java/com/diffplug/spotless/annotations/Internal.java +++ b/lib/src/main/java/com/diffplug/spotless/annotations/Internal.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ /** * Signifies that a {@code public} API is actually an implementation detail, and should be treated as if it * were {@code private}. - * + *

* The user of the API should be warned that it may unexpectedly disappear in future versions of * Spotless. Usually the best place to put this warning is in the API's class JavaDoc. */ diff --git a/lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java b/lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java index d71182c65d..e62ac29332 100644 --- a/lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,44 +15,53 @@ */ package com.diffplug.spotless.antlr4; -import java.io.IOException; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import com.diffplug.spotless.*; - -public class Antlr4FormatterStep { +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.ThrowingEx; +public class Antlr4FormatterStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String MAVEN_COORDINATE = "com.khubla.antlr4formatter:antlr4-formatter:"; + private static final String DEFAULT_VERSION = "1.2.1"; public static final String NAME = "antlr4Formatter"; - private Antlr4FormatterStep() {} + private final JarState.Promised jarState; - private static final String MAVEN_COORDINATE = "com.khubla.antlr4formatter:antlr4-formatter:"; - private static final String DEFAULT_VERSION = "1.2.1"; + private Antlr4FormatterStep(JarState.Promised jarState) { + this.jarState = jarState; + } public static FormatterStep create(Provisioner provisioner) { return create(defaultVersion(), provisioner); } public static FormatterStep create(String version, Provisioner provisioner) { - return FormatterStep.createLazy(NAME, () -> new State(version, provisioner), State::createFormat); + return FormatterStep.create(NAME, + new Antlr4FormatterStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner))), + Antlr4FormatterStep::equalityState, + State::createFormat); } public static String defaultVersion() { return DEFAULT_VERSION; } - static final class State implements Serializable { - private static final long serialVersionUID = 1L; + private State equalityState() { + return new State(jarState.get()); + } - /** - * The jar that contains the formatter. - */ - final JarState jarState; + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + private final JarState jarState; - State(String version, Provisioner provisioner) throws IOException { - this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner); + State(JarState jarState) { + this.jarState = jarState; } FormatterFunc createFormat() throws ClassNotFoundException, NoSuchMethodException { diff --git a/lib/src/main/java/com/diffplug/spotless/biome/Architecture.java b/lib/src/main/java/com/diffplug/spotless/biome/Architecture.java new file mode 100644 index 0000000000..29511842e2 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/Architecture.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2024 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.biome; + +/** + * Enumeration of possible computer architectures. + */ +enum Architecture { + /** The arm64 architecture */ + ARM64, + /** Either x64 or x64_32 architecture */ + X64; + + /** + * Attempts to guess the architecture of the environment running the JVM. + * + * @return The best guess for the architecture. + */ + public static Architecture guess() { + var arch = System.getProperty("os.arch"); + var version = System.getProperty("os.version"); + + if (arch == null || arch.isBlank()) { + throw new IllegalStateException("No OS information is available, specify the Biome executable manually"); + } + + var msg = "Unsupported architecture " + arch + "/" + version + + ", specify the path to the Biome executable manually"; + + if (arch.equals("ppc64le")) { + throw new IllegalStateException(msg); + } + if (arch.equals("s390x")) { + throw new IllegalStateException(msg); + } + if (arch.equals("ppc64")) { + throw new IllegalStateException(msg); + } + if (arch.equals("ppc")) { + throw new IllegalStateException(msg); + } + if (arch.equals("aarch64")) { + return ARM64; + } + if (arch.equals("arm")) { + if (version.contains("v7")) { + throw new IllegalStateException(msg); + } + return ARM64; + } + if (arch.contains("64")) { + return X64; + } + throw new IllegalStateException(msg); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/biome/BiomeExecutableDownloader.java b/lib/src/main/java/com/diffplug/spotless/biome/BiomeExecutableDownloader.java new file mode 100644 index 0000000000..c01d11a20c --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/BiomeExecutableDownloader.java @@ -0,0 +1,373 @@ +/* + * Copyright 2016-2024 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.biome; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Downloader for the Biome executable: + * https://github.com/biomejs/biome. + */ +final class BiomeExecutableDownloader { + private static final Logger logger = LoggerFactory.getLogger(BiomeExecutableDownloader.class); + + /** + * The checksum algorithm to use for checking the integrity of downloaded files. + */ + private static final String CHECKSUM_ALGORITHM = "SHA-256"; + + /** + * The pattern for {@link String#format(String, Object...) String.format()} for + * the platform part of the Biome executable download URL. First parameter is the + * OS, second parameter the architecture, the third the file extension. + */ + private static final String PLATFORM_PATTERN = "%s-%s%s"; + + /** + * {@link OpenOption Open options} for reading an existing file without write + * access. + */ + private static final OpenOption[] READ_OPTIONS = {StandardOpenOption.READ}; + + /** + * {@link OpenOption Open options} for creating a new file, overwriting the + * existing file if present. + */ + private static final OpenOption[] WRITE_OPTIONS = {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE}; + + private final Path downloadDir; + + private final BiomeFlavor flavor; + + /** + * Creates a new downloader for the Biome executable. The executable files are + * stored in the given download directory. + * + * @param flavor Flavor of Biome to use. + * @param downloadDir Directory where to store the downloaded executable. + */ + public BiomeExecutableDownloader(BiomeFlavor flavor, Path downloadDir) { + this.flavor = flavor; + this.downloadDir = downloadDir; + } + + /** + * Downloads the Biome executable for the current platform from the network to + * the download directory. When the executable exists already, it is + * overwritten. + * + * @param version Desired Biome version. + * @return The path to the Biome executable. + * @throws IOException When the executable cannot be downloaded from + * the network or the file system could not be + * accessed. + * @throws InterruptedException When this thread was interrupted while + * downloading the file. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Path download(String version) throws IOException, InterruptedException { + var platform = Platform.guess(); + var url = getDownloadUrl(version, platform); + var executablePath = getExecutablePath(version, platform); + var checksumPath = getChecksumPath(executablePath); + var executableDir = executablePath.getParent(); + if (executableDir != null) { + Files.createDirectories(executableDir); + } + logger.info("Attempting to download Biome from '{}' to '{}'", url, executablePath); + var request = HttpRequest.newBuilder(URI.create(url)).GET().build(); + var handler = BodyHandlers.ofFile(executablePath, WRITE_OPTIONS); + var response = HttpClient.newBuilder().followRedirects(Redirect.NORMAL).build().send(request, handler); + if (response.statusCode() != 200) { + throw new IOException("Failed to download file from " + url + ", server returned " + response.statusCode()); + } + var downloadedFile = response.body(); + if (!Files.exists(downloadedFile) || Files.size(downloadedFile) == 0) { + throw new IOException("Failed to download file from " + url + ", file is empty or does not exist"); + } + writeChecksumFile(downloadedFile, checksumPath); + logger.debug("Biome was downloaded successfully to '{}'", downloadedFile); + return downloadedFile; + } + + /** + * Ensures that the Biome executable for the current platform exists in the + * download directory. When the executable does not exist in the download + * directory, an attempt is made to download the Biome executable from the + * network. When the executable exists already, no attempt to download it again + * is made. + * + * @param version Desired Biome version. + * @return The path to the Biome executable. + * @throws IOException When the executable cannot be downloaded from + * the network or the file system could not be + * accessed. + * @throws InterruptedException When this thread was interrupted while + * downloading the file. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Path ensureDownloaded(String version) throws IOException, InterruptedException { + var platform = Platform.guess(); + logger.debug("Ensuring that Biome for platform '{}' is downloaded", platform); + var existing = findDownloaded(version); + if (existing.isPresent()) { + logger.debug("Biome was already downloaded, using executable at '{}'", existing.get()); + return existing.get(); + } else { + logger.debug("Biome was not yet downloaded, attempting to download executable"); + return download(version); + } + } + + /** + * Attempts to find the Biome executable for the current platform in the download + * directory. No attempt is made to download the executable from the network. + * + * @param version Desired Biome version. + * @return The path to the Biome executable. + * @throws IOException When the executable does not exists in the + * download directory, or when the file system + * could not be accessed. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Optional findDownloaded(String version) throws IOException { + var platform = Platform.guess(); + var executablePath = getExecutablePath(version, platform); + logger.debug("Checking Biome executable at {}", executablePath); + return checkFileWithChecksum(executablePath) ? Optional.ofNullable(executablePath) : Optional.empty(); + } + + /** + * Checks whether the given file exists and matches the checksum. The checksum + * must be contained in a file next to the file to check. + * + * @param filePath File to check. + * @return true if the file exists and matches the checksum, + * false otherwise. + */ + private boolean checkFileWithChecksum(Path filePath) { + if (!Files.exists(filePath)) { + logger.debug("File '{}' does not exist yet", filePath); + return false; + } + if (Files.isDirectory(filePath)) { + logger.debug("File '{}' exists, but is a directory", filePath); + return false; + } + var checksumPath = getChecksumPath(filePath); + if (!Files.exists(checksumPath)) { + logger.debug("File '{}' exists, but checksum file '{}' does not", filePath, checksumPath); + return false; + } + if (Files.isDirectory(checksumPath)) { + logger.debug("Checksum file '{}' exists, but is a directory", checksumPath); + return false; + } + try { + var actualChecksum = computeChecksum(filePath, CHECKSUM_ALGORITHM); + var expectedChecksum = readTextFile(checksumPath, StandardCharsets.ISO_8859_1); + logger.debug("Expected checksum: {}, actual checksum: {}", expectedChecksum, actualChecksum); + return Objects.equals(expectedChecksum, actualChecksum); + } catch (final IOException ignored) { + return false; + } + } + + /** + * Computes the checksum of the given file. + * + * @param file File to process. + * @param algorithm The checksum algorithm to use. + * @return The checksum of the given file. + * @throws IOException When the file does not exist or could not be read. + */ + private String computeChecksum(Path file, String algorithm) throws IOException { + var buffer = new byte[4192]; + try (var in = Files.newInputStream(file, READ_OPTIONS)) { + var digest = MessageDigest.getInstance(algorithm); + int result; + while ((result = in.read(buffer, 0, buffer.length)) != -1) { + digest.update(buffer, 0, result); + } + var bytes = digest.digest(); + return String.format("%0" + (bytes.length * 2) + "X", new BigInteger(1, bytes)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Finds the code name for the given operating system used by the Biome + * executable download URL. + * + * @param os Desired operating system. + * @return Code name for the Biome download URL. + * @throws IOException When the given OS is not supported by Biome. + */ + private String getArchitectureCodeName(Architecture architecture) throws IOException { + switch (architecture) { + case ARM64: + return "arm64"; + case X64: + return "x64"; + default: + throw new IOException("Unsupported architecture: " + architecture); + } + } + + /** + * Derives a path for the file which contains the checksum of the given file. + * + * @param file A file for which to derive the checksum file path. + * @return The path with the checksum for the given file. + */ + private Path getChecksumPath(Path file) { + var parent = file.getParent(); + var base = parent != null ? parent : file; + var fileName = file.getFileName(); + var checksumName = fileName != null ? fileName.toString() + ".sha256" : "checksum.sha256"; + return base.resolve(checksumName); + } + + /** + * Finds the URL from which the Biome executable can be downloaded. + * + * @param version Desired Biome version. + * @param platform Desired platform. + * @return The URL for the Biome executable. + * @throws IOException When the platform is not supported by Biome. + */ + private String getDownloadUrl(String version, Platform platform) throws IOException { + var osCodeName = getOsCodeName(platform.getOs()); + var architectureCodeName = getArchitectureCodeName(platform.getArchitecture()); + var extension = getDownloadUrlExtension(platform.getOs()); + var platformString = String.format(PLATFORM_PATTERN, osCodeName, architectureCodeName, extension); + return String.format(flavor.getUrlPattern(), version, platformString); + } + + /** + * Finds the file extension of the Biome download URL for the given operating + * system. + * + * @param os Desired operating system. + * @return Extension for the Biome download URL. + * @throws IOException When the given OS is not supported by Biome. + */ + private String getDownloadUrlExtension(OS os) throws IOException { + switch (os) { + case LINUX: + return ""; + case MAC_OS: + return ""; + case WINDOWS: + return ".exe"; + default: + throw new IOException("Unsupported OS: " + os); + } + } + + /** + * Finds the path on the file system for the Biome executable with a given + * version and platform. + * + * @param version Desired Biome version. + * @param platform Desired platform. + * @return The path for the Biome executable. + */ + private Path getExecutablePath(String version, Platform platform) { + var os = platform.getOs().name().toLowerCase(Locale.ROOT); + var arch = platform.getArchitecture().name().toLowerCase(Locale.ROOT); + var fileName = String.format(flavor.getDownloadFilePattern(), os, arch, version); + return downloadDir.resolve(fileName); + } + + /** + * Finds the code name for the given operating system used by the Biome + * executable download URL. + * + * @param os Desired operating system. + * @return Code name for the Biome download URL. + * @throws IOException When the given OS is not supported by Biome. + */ + private String getOsCodeName(OS os) throws IOException { + switch (os) { + case LINUX: + return "linux"; + case MAC_OS: + return "darwin"; + case WINDOWS: + return "win32"; + default: + throw new IOException("Unsupported OS: " + os); + } + } + + /** + * Reads a plain text file with the given encoding into a string. + * + * @param file File to read. + * @param charset Encoding to use. + * @return The contents of the file as a string. + * @throws IOException When the file could not be read. + */ + private String readTextFile(Path file, Charset charset) throws IOException { + try (var in = Files.newInputStream(file, READ_OPTIONS)) { + return new String(in.readAllBytes(), charset); + } + } + + /** + * Computes the checksum of the given file and writes it to the target checksum + * file, using the {@code ISO_8859_1} encoding. + * + * @param file + * @param checksumPath + * @throws IOException + */ + private void writeChecksumFile(Path file, Path checksumPath) throws IOException { + var checksum = computeChecksum(file, CHECKSUM_ALGORITHM); + try (var out = Files.newOutputStream(checksumPath, WRITE_OPTIONS)) { + out.write(checksum.getBytes(StandardCharsets.ISO_8859_1)); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/biome/BiomeFlavor.java b/lib/src/main/java/com/diffplug/spotless/biome/BiomeFlavor.java new file mode 100644 index 0000000000..bcb065edea --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/BiomeFlavor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023-2024 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.biome; + +/** + * The flavor of Biome to use. Exists for compatibility reason, may be removed + * shortly. + *

+ * Will be removed once the old Rome project is not supported anymore. + */ +public enum BiomeFlavor { + /** The new forked Biome project. */ + BIOME("biome", "1.2.0", "biome.json", "biome-%s-%s-%s", + "https://github.com/biomejs/biome/releases/download/cli%%2Fv%s/biome-%s"); + + private final String configName; + private final String defaultVersion; + private final String downloadFilePattern; + private final String shortName; + private final String urlPattern; + + BiomeFlavor(String shortName, String defaultVersion, String configName, String downloadFilePattern, + String urlPattern) { + this.shortName = shortName; + this.defaultVersion = defaultVersion; + this.configName = configName; + this.downloadFilePattern = downloadFilePattern; + this.urlPattern = urlPattern; + } + + /** + * @return The name of the default config file. + */ + public String configName() { + return configName; + } + + /** + * @return Default version to use when no version was set explicitly. + */ + public String defaultVersion() { + return defaultVersion; + } + + /** + * @return The pattern for {@link String#format(String, Object...) + * String.format()} for the file name of a Biome executable for a + * certain version and architecure. The first parameter is the platform, + * the second is the OS, the third is the architecture. + */ + public String getDownloadFilePattern() { + return downloadFilePattern; + } + + /** + * @return The pattern for {@link String#format(String, Object...) + * String.format()} for the URL where the executables can be downloaded. + * The first parameter is the version, the second parameter is the OS / + * platform. + */ + public String getUrlPattern() { + return urlPattern; + } + + /** + * @return The short name of this flavor, i.e. rome or + * biome. + */ + public String shortName() { + return shortName; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java b/lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java new file mode 100644 index 0000000000..09a42ae4a0 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/BiomeStep.java @@ -0,0 +1,508 @@ +/* + * Copyright 2016-2024 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.biome; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +/** + * formatter step that formats JavaScript and TypeScript code with Biome: + * https://github.com/biomejs/biome. + * It delegates to the Biome executable. The Biome executable is downloaded from + * the network when no executable path is provided explicitly. + */ +public class BiomeStep { + private static final Logger logger = LoggerFactory.getLogger(BiomeStep.class); + + /** + * Path to the directory with the {@code biome.json} config file, can be + * null, in which case the defaults are used. + */ + private String configPath; + + /** + * The language (syntax) of the input files to format. When null or + * the empty string, the language is detected automatically from the file name. + * Currently, the following languages are supported by Biome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ */ + private String language; + + /** + * Biome flavor to use. Will be removed once we stop supporting the deprecated Rome project. + */ + @Deprecated + private final BiomeFlavor flavor; + + /** + * Path to the Biome executable. Can be null, but either a path to + * the executable of a download directory and version must be given. The path + * must be either an absolute path, or a file name without path separators. If + * the latter, it is interpreted as a command in the user's path. + */ + private final String pathToExe; + + /** + * Absolute path to the download directory for storing the download Biome + * executable. Can be null, but either a path to the executable of + * a download directory and version must be given. + */ + private final String downloadDir; + + /** + * Version of Biome to download. Can be null, but either a path to + * the executable of a download directory and version must be given. + */ + private final String version; + + /** + * @return The name of this format step, i.e. biome or rome. + */ + public String name() { + return flavor.shortName(); + } + + /** + * Creates a Biome step that format code by downloading to the given Biome + * version. The executable is downloaded from the network. + * + * @param flavor Flavor of Biome to use. + * @param version Version of the Biome executable to download. + * @param downloadDir Directory where to place the downloaded executable. + * @return A new Biome step that download the executable from the network. + */ + public static BiomeStep withExeDownload(BiomeFlavor flavor, String version, String downloadDir) { + return new BiomeStep(flavor, version, null, downloadDir); + } + + /** + * Creates a Biome step that formats code by delegating to the Biome executable + * located at the given path. + * + * @param flavor Flavor of Biome to use. + * @param pathToExe Path to the Biome executable to use. + * @return A new Biome step that format with the given executable. + */ + public static BiomeStep withExePath(BiomeFlavor flavor, String pathToExe) { + return new BiomeStep(flavor, null, pathToExe, null); + } + + /** + * Attempts to add a POSIX permission to the given file, ignoring any errors. + * All existing permissions on the file are preserved and the new permission is + * added, if possible. + * + * @param file File or directory to which to add a permission. + * @param permission The POSIX permission to add. + */ + private static void attemptToAddPosixPermission(Path file, PosixFilePermission permission) { + try { + var newPermissions = new HashSet<>(Files.getPosixFilePermissions(file)); + newPermissions.add(permission); + Files.setPosixFilePermissions(file, newPermissions); + } catch (final Exception ignore) { + logger.debug("Unable to add POSIX permission '{}' to file '{}'", permission, file); + } + } + + /** + * Finds the default version for Biome when no version is specified explicitly. + * Over time this will become outdated -- people should always specify the + * version explicitly! + * + * @return The default version for Biome. + */ + private static String defaultVersion(BiomeFlavor flavor) { + return flavor.defaultVersion(); + } + + /** + * Attempts to make the given file executable. This is a best-effort attempt, + * any errors are swallowed. Depending on the OS, the file might still be + * executable even if this method fails. The user will get a descriptive error + * later when we attempt to execute the Biome executable. + * + * @param filePath Path to the file to make executable. + */ + private static void makeExecutable(String filePath) { + var exePath = Paths.get(filePath); + attemptToAddPosixPermission(exePath, PosixFilePermission.GROUP_EXECUTE); + attemptToAddPosixPermission(exePath, PosixFilePermission.OTHERS_EXECUTE); + attemptToAddPosixPermission(exePath, PosixFilePermission.OWNER_EXECUTE); + } + + /** + * Finds the absolute path of a command on the user's path. Uses {@code which} + * for Linux and {@code where} for Windows. + * + * @param name Name of the command to resolve. + * @return The absolute path of the command's executable. + * @throws IOException When the command could not be resolved. + * @throws InterruptedException When this thread was interrupted while waiting + * to the which command to finish. + */ + private static String resolveNameAgainstPath(String name) throws IOException, InterruptedException { + try (var runner = new ProcessRunner()) { + var cmdWhich = runner.shellWinUnix("where " + name, "which " + name); + if (cmdWhich.exitNotZero()) { + throw new IOException("Unable to find " + name + " on path via command " + cmdWhich); + } else { + return cmdWhich.assertExitZero(Charset.defaultCharset()).trim(); + } + } + } + + /** + * Checks the Biome config path. When the config path does not exist or when it + * does not contain a file named {@code biome.json}, an error is thrown. + */ + private static void validateBiomeConfigPath(BiomeFlavor flavor, String configPath) { + if (configPath == null) { + return; + } + var path = Paths.get(configPath); + var config = path.resolve(flavor.configName()); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Biome config directory does not exist: " + path); + } + if (!Files.exists(config)) { + throw new IllegalArgumentException("Biome config does not exist: " + config); + } + } + + /** + * Checks the Biome executable file. When the file does not exist, an error is + * thrown. + */ + private static void validateBiomeExecutable(String resolvedPathToExe) { + if (!new File(resolvedPathToExe).isFile()) { + throw new IllegalArgumentException("Biome executable does not exist: " + resolvedPathToExe); + } + } + + /** + * Creates a new Biome step with the configuration from the given builder. + * + * @param flavor Flavor of Biome to use. + * @param version Version of the Biome executable to download. + * @param pathToExe Path to the Biome executable to use. + * @param downloadDir Directory where to place the downloaded executable. + */ + private BiomeStep(BiomeFlavor flavor, String version, String pathToExe, String downloadDir) { + this.flavor = flavor; + this.version = version != null && !version.isBlank() ? version : defaultVersion(flavor); + this.pathToExe = pathToExe; + this.downloadDir = downloadDir; + } + + /** + * Creates a formatter step with the current configuration, which formats code + * by passing it to the Biome executable. + * + * @return A new formatter step for formatting with Biome. + */ + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createState, State::toFunc); + } + + /** + * Sets the path to the directory with the {@code biome.json} config file. When + * no config path is set, the default configuration is used. + * + * @param configPath Config path to use. Must point to a directory which contains + * a file named {@code biome.json}. + * @return This builder instance for chaining method calls. + */ + public BiomeStep withConfigPath(String configPath) { + this.configPath = configPath; + return this; + } + + /** + * Sets the language of the files to format When no language is set, it is + * determined automatically from the file name. The following languages are + * currently supported by Biome. + * + *
    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ * + * @param language The language of the files to format. + * @return This builder instance for chaining method calls. + */ + public BiomeStep withLanguage(String language) { + this.language = language; + return this; + } + + /** + * Resolves the Biome executable, possibly downloading it from the network, and + * creates a new state instance with the resolved executable that can format + * code via Biome. + * + * @return The state instance for formatting code via Biome. + * @throws IOException When any file system or network operations + * failed, such as when the Biome executable could + * not be downloaded, or when the given executable + * does not exist. + * @throws InterruptedException When the Biome executable needs to be downloaded + * and this thread was interrupted while waiting + * for the download to complete. + */ + private State createState() throws IOException, InterruptedException { + var resolvedPathToExe = resolveExe(); + validateBiomeExecutable(resolvedPathToExe); + validateBiomeConfigPath(flavor, configPath); + logger.debug("Using Biome executable located at '{}'", resolvedPathToExe); + var exeSignature = FileSignature.signAsList(Collections.singleton(new File(resolvedPathToExe))); + makeExecutable(resolvedPathToExe); + return new State(resolvedPathToExe, exeSignature, configPath, language); + } + + /** + * Resolves the path to the Biome executable, given the configuration of this + * step. When the path to the Biome executable is given explicitly, that path is + * used as-is. Otherwise, at attempt is made to download the Biome executable for + * the configured version from the network, unless it was already downloaded and + * is available in the cache. + * + * @return The path to the resolved Biome executable. + * @throws IOException When any file system or network operations + * failed, such as when the Biome executable could + * not be downloaded. + * @throws InterruptedException When the Biome executable needs to be downloaded + * and this thread was interrupted while waiting + * for the download to complete. + */ + private String resolveExe() throws IOException, InterruptedException { + new ForeignExe(); + if (pathToExe != null) { + if (Paths.get(pathToExe).getNameCount() == 1) { + return resolveNameAgainstPath(pathToExe); + } else { + return pathToExe; + } + } else { + var downloader = new BiomeExecutableDownloader(flavor, Paths.get(downloadDir)); + var downloaded = downloader.ensureDownloaded(version).toString(); + makeExecutable(downloaded); + return downloaded; + } + } + + /** + * The internal state used by the Biome formatter. A state instance is created + * when the spotless plugin for Maven or Gradle is executed, and reused for all + * formatting requests for different files. The lifetime of the instance ends + * when the Maven or Gradle plugin was successfully executed. + *

+ * The state encapsulated a particular executable. It is serializable for + * caching purposes. Spotless keeps a cache of which files need to be formatted. + * The cache is busted when the serialized form of a state instance changes. + */ + private static class State implements Serializable { + private static final long serialVersionUID = 6846790911054484379L; + + /** Path to the exe file */ + private final String pathToExe; + + /** The signature of the exe file, if any, used for caching. */ + @SuppressWarnings("unused") + private final FileSignature exeSignature; + + /** + * The optional path to the directory with the {@code biome.json} config file. + */ + private final String configPath; + + /** + * The language of the files to format. When null or the empty + * string, the language is detected from the file name. + */ + private final String language; + + /** + * Creates a new state for instance which can format code with the given Biome + * executable. + * + * @param exe Path to the Biome executable. + * @param exeSignature Signature (e.g. SHA-256 checksum) of the Biome executable. + * @param configPath Path to the optional directory with the {@code biome.json} + * config file, can be null, in which case the + * defaults are used. + */ + private State(String exe, FileSignature exeSignature, String configPath, String language) { + this.pathToExe = exe; + this.exeSignature = exeSignature; + this.configPath = configPath; + this.language = language; + } + + /** + * Builds the list of arguments for the command that executes Biome to format a + * piece of code passed via stdin. + * + * @param file File to format. + * @return The Biome command to use for formatting code. + */ + private String[] buildBiomeCommand(File file) { + var fileName = resolveFileName(file); + var argList = new ArrayList(); + argList.add(pathToExe); + argList.add("format"); + argList.add("--stdin-file-path"); + argList.add(fileName); + if (configPath != null) { + argList.add("--config-path"); + argList.add(configPath); + } + return argList.toArray(String[]::new); + } + + /** + * Formats the given piece of code by delegating to the Biome executable. The + * code is passed to Biome via stdin, the file name is used by Biome only to + * determine the code syntax (e.g. JavaScript or TypeScript). + * + * @param runner Process runner for invoking the Biome executable. + * @param input Code to format. + * @param file File to format. + * @return The formatted code. + * @throws IOException When a file system error occurred while + * executing Biome. + * @throws InterruptedException When this thread was interrupted while waiting + * for Biome to finish formatting. + */ + private String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + var stdin = input.getBytes(StandardCharsets.UTF_8); + var args = buildBiomeCommand(file); + if (logger.isDebugEnabled()) { + logger.debug("Running Biome command to format code: '{}'", String.join(", ", args)); + } + var runnerResult = runner.exec(stdin, args); + var stdErr = runnerResult.stdErrUtf8(); + if (!stdErr.isEmpty()) { + logger.warn("Biome stderr ouptut for file '{}'\n{}", file, stdErr.trim()); + } + var formatted = runnerResult.assertExitZero(StandardCharsets.UTF_8); + // When biome encounters an ignored file, it does not output any formatted code + // Ignored files come from (a) the biome.json configuration file and (b) from + // a list of hard-coded file names, such as package.json or tsconfig.json. + if (formatted.isEmpty()) { + return input; + } else { + return formatted; + } + } + + /** + * The Biome executable currently does not have a parameter to specify the + * expected language / syntax. Biome always determined the language from the file + * extension. This method returns the file name for the desired language when a + * language was requested explicitly, or the file name of the input file for + * auto-detection. + * + * @param file File to be formatted. + * @return The file name to pass to the Biome executable. + */ + private String resolveFileName(File file) { + var name = file.getName(); + if (language == null || language.isBlank()) { + return name; + } + var dot = name.lastIndexOf("."); + var ext = dot >= 0 ? name.substring(dot + 1) : name; + switch (language) { + case "js?": + return "jsx".equals(ext) || "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name + : "file.js"; + case "ts?": + return "tsx".equals(ext) || "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name + : "file.js"; + case "js": + return "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name : "file.js"; + case "jsx": + return "jsx".equals(ext) ? name : "file.jsx"; + case "ts": + return "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name : "file.ts"; + case "tsx": + return "tsx".equals(ext) ? name : "file.tsx"; + case "json": + return "json".equals(ext) ? name : "file.json"; + case "jsonc": + return "jsonc".equals(ext) ? name : "file.jsonc"; + case "css": + return "css".equals(ext) ? name : "file.css"; + // so that we can support new languages such as css or yaml when Biome adds + // support for them without having to change the code + default: + return "file." + language; + } + } + + /** + * Creates a new formatter function for formatting a piece of code by delegating + * to the Biome executable. + * + * @return A formatter function for formatting code. + */ + private FormatterFunc.Closeable toFunc() { + var runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/biome/OS.java b/lib/src/main/java/com/diffplug/spotless/biome/OS.java new file mode 100644 index 0000000000..ab0ba4b8db --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/OS.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2024 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.biome; + +import java.util.Locale; + +/** + * Enumeration of possible computer operation systems. + */ +enum OS { + /** Any derivate of a Linux operation system. */ + LINUX, + /** The Macintosh operating system/ */ + MAC_OS, + /** The Microsoft Windows operating system. */ + WINDOWS,; + + /** + * Attempts to guess the OS of the environment running the JVM. + * + * @return The best guess for the architecture. + * @throws IllegalStateException When the OS is either unsupported or no + * information about the OS could be retrieved. + */ + public static OS guess() { + var osName = System.getProperty("os.name"); + if (osName == null || osName.isBlank()) { + throw new IllegalStateException("No OS information is available, specify the Biome executable manually"); + } + var osNameUpper = osName.toUpperCase(Locale.ROOT); + if (osNameUpper.contains("SUNOS") || osName.contains("AIX")) { + throw new IllegalStateException( + "Unsupported OS " + osName + ", specify the path to the Biome executable manually"); + } + if (osNameUpper.contains("WINDOWS")) { + return OS.WINDOWS; + } else if (osNameUpper.contains("MAC")) { + return OS.MAC_OS; + } else { + return OS.LINUX; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/biome/Platform.java b/lib/src/main/java/com/diffplug/spotless/biome/Platform.java new file mode 100644 index 0000000000..590e303d98 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/biome/Platform.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2024 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.biome; + +/** + * Represents a platform where code is run, consisting of an operating system + * and an architecture. + */ +class Platform { + /** + * Attempts to guess the platform of the hosting environment running the JVM + * machine. + * + * @return The best guess for the current OS and architecture. + * @throws IllegalStateException When no OS information is available, or when + * the OS or architecture is unsupported. + */ + public static Platform guess() { + var os = OS.guess(); + var architecture = Architecture.guess(); + return new Platform(os, architecture); + } + + private final Architecture architecture; + + private final OS os; + + /** + * Creates a new Platform descriptor for the given OS and architecture. + * + * @param os Operating system of the platform. + * @param architecture Architecture of the platform. + */ + public Platform(OS os, Architecture architecture) { + this.os = os; + this.architecture = architecture; + } + + /** + * @return The architecture of this platform. + */ + public Architecture getArchitecture() { + return architecture; + } + + /** + * @return The operating system of this platform. + */ + public OS getOs() { + return os; + } + + /** + * @return Whether the operating system is Linux. + */ + public boolean isLinux() { + return os == OS.LINUX; + } + + /** + * @return Whether the operating system is Mac. + */ + public boolean isMac() { + return os == OS.MAC_OS; + } + + /** + * @return Whether the operating system is Windows. + */ + public boolean isWindows() { + return os == OS.WINDOWS; + } + + @Override + public String toString() { + return String.format("Platform[os=%s,architecture=%s]", os, architecture); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 5256739004..64a43bf439 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javax.annotation.Nullable; @@ -63,10 +64,10 @@ public ClangFormatStep withPathToExe(String pathToExe) { } public FormatterStep create() { - return FormatterStep.createLazy(name(), this::createState, State::toFunc); + return FormatterStep.createLazy(name(), this::createRoundtrip, RoundtripState::toEquality, EqualityState::toFunc); } - private State createState() throws IOException, InterruptedException { + private RoundtripState createRoundtrip() throws IOException, InterruptedException { String howToInstall = "" + "You can download clang-format from https://releases.llvm.org and " + "then point Spotless to it with {@code pathToExe('/path/to/clang-format')} " + @@ -75,39 +76,60 @@ private State createState() throws IOException, InterruptedException { "\n mac: brew install clang-format (TODO: how to specify version?)" + "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)" + "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/673"; - String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version) + final ForeignExe exe = ForeignExe.nameAndVersion("clang-format", version) .pathToExe(pathToExe) .fixCantFind(howToInstall) .fixWrongVersion( "You can tell Spotless to use the version you already have with {@code clangFormat('{versionFound}')}" + - "or you can download the currently specified version, {version}.\n" + howToInstall) - .confirmVersionAndGetAbsolutePath(); - return new State(this, exeAbsPath); + "or you can download the currently specified version, {version}.\n" + howToInstall); + return new RoundtripState(this, exe); + } + + static class RoundtripState implements Serializable { + private static final long serialVersionUID = 1L; + + final String version; + final @Nullable String style; + final ForeignExe exe; + + RoundtripState(ClangFormatStep step, ForeignExe exe) { + this.version = step.version; + this.style = step.style; + this.exe = exe; + } + + private EqualityState toEquality() { + return new EqualityState(version, style, exe); + } } @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - static class State implements Serializable { + static class EqualityState implements Serializable { private static final long serialVersionUID = -1825662356883926318L; // used for up-to-date checks and caching final String version; final @Nullable String style; + final transient ForeignExe exe; // used for executing - final transient List args; + private transient @Nullable List args; - State(ClangFormatStep step, String exeAbsPath) { - this.version = step.version; - this.style = step.style; - args = new ArrayList<>(2); - args.add(exeAbsPath); - if (style != null) { - args.add("--style=" + style); - } + EqualityState(String version, @Nullable String style, ForeignExe pathToExe) { + this.version = version; + this.style = style; + this.exe = Objects.requireNonNull(pathToExe); } String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { - String[] processArgs = args.toArray(new String[args.size() + 1]); - // add an argument to the end - processArgs[args.size()] = "--assume-filename=" + file.getName(); + if (args == null) { + final List tmpArgs = new ArrayList<>(); + tmpArgs.add(exe.confirmVersionAndGetAbsolutePath()); + if (style != null) { + tmpArgs.add("--style=" + style); + } + args = tmpArgs; + } + final String[] processArgs = args.toArray(new String[args.size() + 1]); + processArgs[processArgs.length - 1] = "--assume-filename=" + file.getName(); return runner.exec(input.getBytes(StandardCharsets.UTF_8), processArgs).assertExitZero(StandardCharsets.UTF_8); } diff --git a/lib/src/main/java/com/diffplug/spotless/generic/FenceStep.java b/lib/src/main/java/com/diffplug/spotless/generic/FenceStep.java new file mode 100644 index 0000000000..7d98219231 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/generic/FenceStep.java @@ -0,0 +1,244 @@ +/* + * Copyright 2020-2025 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.generic; + +import java.io.File; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.diffplug.spotless.ConfigurationCacheHackList; +import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.Lint; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class FenceStep { + /** Declares the name of the step. */ + public static FenceStep named(String name) { + return new FenceStep(name); + } + + public static String defaultToggleName() { + return "toggle"; + } + + public static String defaultToggleOff() { + return "spotless:off"; + } + + public static String defaultToggleOn() { + return "spotless:on"; + } + + String name; + Pattern regex; + + private FenceStep(String name) { + this.name = Objects.requireNonNull(name); + } + + /** Defines the opening and closing markers. */ + public FenceStep openClose(String open, String close) { + return regex(Pattern.quote(open) + "([\\s\\S]*?)" + Pattern.quote(close)); + } + + /** Defines the pipe via regex. Must have *exactly one* capturing group. */ + public FenceStep regex(String regex) { + return regex(Pattern.compile(regex)); + } + + /** Defines the pipe via regex. Must have *exactly one* capturing group. */ + public FenceStep regex(Pattern regex) { + this.regex = Objects.requireNonNull(regex); + return this; + } + + private void assertRegexSet() { + Objects.requireNonNull(regex, "must call regex() or openClose()"); + } + + /** Returns a step which will apply the given steps but preserve the content selected by the regex / openClose pair. */ + public FormatterStep preserveWithin(List steps) { + return createStep(Kind.PRESERVE, steps); + } + + /** + * Returns a step which will apply the given steps only within the blocks selected by the regex / openClose pair. + * Linting within the substeps is not supported. + */ + public FormatterStep applyWithin(List steps) { + return createStep(Kind.APPLY, steps); + } + + private FormatterStep createStep(Kind kind, List steps) { + assertRegexSet(); + return FormatterStep.createLazy(name, () -> new RoundtripAndEqualityState(kind, regex, steps, false), + RoundtripAndEqualityState::toEqualityState, + RoundtripAndEqualityState::toFormatterFunc); + } + + private enum Kind { + APPLY, PRESERVE + } + + private static class RoundtripAndEqualityState implements Serializable { + private static final long serialVersionUID = 272603249547598947L; + final String regexPattern; + final int regexFlags; + final Kind kind; + final ConfigurationCacheHackList steps; + + /** Roundtrip state. */ + private RoundtripAndEqualityState(Kind kind, Pattern regex, List steps, boolean optimizeForEquality) { + this.kind = kind; + this.regexPattern = regex.pattern(); + this.regexFlags = regex.flags(); + this.steps = optimizeForEquality ? ConfigurationCacheHackList.forEquality() : ConfigurationCacheHackList.forRoundtrip(); + this.steps.addAll(steps); + } + + private Pattern regex() { + return Pattern.compile(regexPattern, regexFlags); + } + + private List steps() { + return steps.getSteps(); + } + + public RoundtripAndEqualityState toEqualityState() { + return new RoundtripAndEqualityState(kind, regex(), steps(), true); + } + + public BaseFormatter toFormatterFunc() { + return new BaseFormatter(kind, this); + } + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + private static class BaseFormatter implements FormatterFunc.NeedsFile, FormatterFunc.Closeable { + final Kind kind; + final Pattern regex; + final List steps; + + final ArrayList groups = new ArrayList<>(); + final StringBuilder builderInternal = new StringBuilder(); + + public BaseFormatter(Kind kind, RoundtripAndEqualityState state) { + this.kind = kind; + this.regex = state.regex(); + this.steps = state.steps(); + } + + protected ArrayList groupsZeroed() { + groups.clear(); + return groups; + } + + private StringBuilder builderZeroed() { + builderInternal.setLength(0); + return builderInternal; + } + + protected Formatter buildFormatter() { + return Formatter.builder() + .encoding(StandardCharsets.UTF_8) // can be any UTF, doesn't matter + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) // just internal, won't conflict with user + .steps(steps) + .build(); + } + + protected String assembleGroups(String unix) { + if (groups.isEmpty()) { + return unix; + } + StringBuilder builder = builderZeroed(); + Matcher matcher = regex.matcher(unix); + int lastEnd = 0; + int groupIdx = 0; + while (matcher.find()) { + builder.append(unix, lastEnd, matcher.start(1)); + builder.append(groups.get(groupIdx)); + lastEnd = matcher.end(1); + ++groupIdx; + } + if (groupIdx == groups.size()) { + builder.append(unix, lastEnd, unix.length()); + return builder.toString(); + } else { + // these will be needed to generate Lints later on + int startLine = 1 + (int) builder.toString().codePoints().filter(c -> c == '\n').count(); + int endLine = 1 + (int) unix.codePoints().filter(c -> c == '\n').count(); + + // throw an error with either the full regex, or the nicer open/close pair + Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E") + .matcher(regex.pattern()); + String pattern; + if (openClose.matches()) { + pattern = openClose.group(1) + " " + openClose.group(2); + } else { + pattern = regex.pattern(); + } + throw Lint.atLineRange(startLine, endLine, "fenceRemoved", + "An intermediate step removed a match of " + pattern).shortcut(); + } + } + + private Formatter formatter; + + @Override + public String applyWithFile(String unix, File file) throws Exception { + if (formatter == null) { + formatter = buildFormatter(); + } + List groups = groupsZeroed(); + Matcher matcher = regex.matcher(unix); + switch (kind) { + case APPLY: + while (matcher.find()) { + // apply the formatter to each group + groups.add(formatter.compute(matcher.group(1), file)); + } + // and then assemble the result right away + return assembleGroups(unix); + case PRESERVE: + while (matcher.find()) { + // store whatever is within the open/close tags + groups.add(matcher.group(1)); + } + String formatted = formatter.compute(unix, file); + return assembleGroups(formatted); + default: + throw new Error(); + } + } + + @Override + public void close() { + if (formatter != null) { + formatter.close(); + formatter = null; + } + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java b/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java index 4281df736c..91155b1a79 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,24 @@ package com.diffplug.spotless.generic; import java.io.Serializable; -import java.util.Objects; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.SerializedFunction; /** Simple step which checks for consistent indentation characters. */ -public final class IndentStep { +public final class IndentStep implements Serializable { + private static final long serialVersionUID = 1L; - private static final int DEFAULT_NUM_SPACES_PER_TAB = 4; + final Type type; + final int numSpacesPerTab; - // prevent direct instantiation - private IndentStep() {} + private IndentStep(Type type, int numSpacesPerTab) { + this.type = type; + this.numSpacesPerTab = numSpacesPerTab; + } + + private static final int DEFAULT_NUM_SPACES_PER_TAB = 4; public enum Type { TAB, SPACE; @@ -49,32 +55,21 @@ public FormatterStep create(int numSpacesPerTab) { /** Creates a step which will indent with the given type of whitespace, converting between tabs and spaces at the given ratio. */ public static FormatterStep create(Type type, int numSpacesPerTab) { - Objects.requireNonNull(type, "type"); return FormatterStep.create("indentWith" + type.tabSpace("Tabs", "Spaces"), - new State(type, numSpacesPerTab), State::toFormatter); + new IndentStep(type, numSpacesPerTab), SerializedFunction.identity(), + IndentStep::startFormatting); } - private static class State implements Serializable { - private static final long serialVersionUID = 1L; - - final Type type; - final int numSpacesPerTab; - - State(Type type, int numSpacesPerTab) { - this.type = type; - this.numSpacesPerTab = numSpacesPerTab; - } - - FormatterFunc toFormatter() { - return new Runtime(this)::format; - } + private FormatterFunc startFormatting() { + var runtime = new Runtime(this); + return runtime::format; } static class Runtime { - final State state; + final IndentStep state; final StringBuilder builder = new StringBuilder(); - Runtime(State state) { + Runtime(IndentStep state) { this.state = state; } diff --git a/lib/src/main/java/com/diffplug/spotless/generic/Jsr223Step.java b/lib/src/main/java/com/diffplug/spotless/generic/Jsr223Step.java index 90f57b24ef..572bf13b95 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/Jsr223Step.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/Jsr223Step.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 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.util.Objects; import java.util.stream.Collectors; +import javax.annotation.Nullable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; @@ -27,27 +28,42 @@ import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; -public final class Jsr223Step { - // prevent direct instantiation - private Jsr223Step() {} +public final class Jsr223Step implements Serializable { + private static final long serialVersionUID = 1L; + @Nullable + private final JarState.Promised jarState; + private final String engine; + private final String script; - public static FormatterStep create(String name, String dependency, CharSequence engine, CharSequence script, Provisioner provisioner) { + private Jsr223Step(@Nullable JarState.Promised jarState, String engine, String script) { + this.jarState = jarState; + this.engine = engine; + this.script = script; + } + + public static FormatterStep create(String name, @Nullable String dependency, CharSequence engine, CharSequence script, Provisioner provisioner) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(engine, "engine"); Objects.requireNonNull(script, "script"); - return FormatterStep.createLazy(name, - () -> new State(dependency == null ? null : JarState.from(dependency, provisioner), engine, script), + return FormatterStep.create(name, + new Jsr223Step(dependency == null ? null : JarState.promise(() -> JarState.from(dependency, provisioner)), engine.toString(), script.toString()), + Jsr223Step::equalityState, State::toFormatter); } + private State equalityState() { + return new State(jarState == null ? null : jarState.get(), engine, script); + } + private static final class State implements Serializable { private static final long serialVersionUID = 1L; + @Nullable private final JarState jarState; private final String engine; private final String script; - State(JarState jarState, CharSequence engine, CharSequence script) { + State(@Nullable JarState jarState, CharSequence engine, CharSequence script) { this.jarState = jarState; this.engine = engine.toString(); this.script = script.toString(); diff --git a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java index 6a34c08828..941c1c376f 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,17 +31,25 @@ import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.OnMatch; import com.diffplug.spotless.SerializableFileFilter; +import com.diffplug.spotless.SerializedFunction; import com.diffplug.spotless.ThrowingEx; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** Prefixes a license header before the package statement. */ public final class LicenseHeaderStep { + public static final String DEFAULT_JAVA_HEADER_DELIMITER = "(package|import|public|class|module) "; + private static final Logger LOGGER = LoggerFactory.getLogger(LicenseHeaderStep.class); + public enum YearMode { PRESERVE, UPDATE_TO_TODAY, SET_FROM_GIT } @@ -51,7 +59,7 @@ public static LicenseHeaderStep headerDelimiter(String header, String delimiter) } public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier headerLazy, String delimiter) { - return new LicenseHeaderStep(null, null, headerLazy, delimiter, DEFAULT_YEAR_DELIMITER, () -> YearMode.PRESERVE); + return new LicenseHeaderStep(null, null, headerLazy, delimiter, DEFAULT_YEAR_DELIMITER, () -> YearMode.PRESERVE, null); } final String name; @@ -60,14 +68,16 @@ public static LicenseHeaderStep headerDelimiter(ThrowingEx.Supplier head final String delimiter; final String yearSeparator; final Supplier yearMode; + final @Nullable String skipLinesMatching; - private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier yearMode) { + private LicenseHeaderStep(@Nullable String name, @Nullable String contentPattern, ThrowingEx.Supplier headerLazy, String delimiter, String yearSeparator, Supplier yearMode, @Nullable String skipLinesMatching) { this.name = sanitizeName(name); - this.contentPattern = sanitizeContentPattern(contentPattern); + this.contentPattern = sanitizePattern(contentPattern); this.headerLazy = Objects.requireNonNull(headerLazy); this.delimiter = Objects.requireNonNull(delimiter); this.yearSeparator = Objects.requireNonNull(yearSeparator); this.yearMode = Objects.requireNonNull(yearMode); + this.skipLinesMatching = sanitizePattern(skipLinesMatching); } public String getName() { @@ -75,11 +85,11 @@ public String getName() { } public LicenseHeaderStep withName(String name) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } public LicenseHeaderStep withContentPattern(String contentPattern) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } public LicenseHeaderStep withHeaderString(String header) { @@ -87,15 +97,15 @@ public LicenseHeaderStep withHeaderString(String header) { } public LicenseHeaderStep withHeaderLazy(ThrowingEx.Supplier headerLazy) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } public LicenseHeaderStep withDelimiter(String delimiter) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } public LicenseHeaderStep withYearSeparator(String yearSeparator) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } public LicenseHeaderStep withYearMode(YearMode yearMode) { @@ -103,18 +113,29 @@ public LicenseHeaderStep withYearMode(YearMode yearMode) { } public LicenseHeaderStep withYearModeLazy(Supplier yearMode) { - return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode); + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); } - public FormatterStep build() { - FormatterStep formatterStep = null; + public LicenseHeaderStep withSkipLinesMatching(@Nullable String skipLinesMatching) { + return new LicenseHeaderStep(name, contentPattern, headerLazy, delimiter, yearSeparator, yearMode, skipLinesMatching); + } + + private static class SetLicenseHeaderYearsFromGitHistory implements SerializedFunction { + private static final long serialVersionUID = 1L; + + @Override + public FormatterFunc apply(Runtime input) throws Exception { + return FormatterFunc.needsFile(input::setLicenseHeaderYearsFromGitHistory); + } + } + public FormatterStep build() { + FormatterStep formatterStep; if (yearMode.get() == YearMode.SET_FROM_GIT) { - formatterStep = FormatterStep.createNeverUpToDateLazy(name, () -> { + formatterStep = FormatterStep.createLazy(name, () -> { boolean updateYear = false; // doesn't matter - Runtime runtime = new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear); - return FormatterFunc.needsFile(runtime::setLicenseHeaderYearsFromGitHistory); - }); + return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching); + }, new SetLicenseHeaderYearsFromGitHistory()); } else { formatterStep = FormatterStep.createLazy(name, () -> { // by default, we should update the year if the user is using ratchetFrom @@ -130,15 +151,13 @@ public FormatterStep build() { default: throw new IllegalStateException(yearMode.toString()); } - return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear); - }, step -> step::format); + return new Runtime(headerLazy.get(), delimiter, yearSeparator, updateYear, skipLinesMatching); + }, step -> FormatterFunc.needsFile(step::format)); } - if (contentPattern == null) { return formatterStep; } - - return formatterStep.filterByContentPattern(contentPattern); + return formatterStep.filterByContent(OnMatch.INCLUDE, contentPattern); } private String sanitizeName(@Nullable String name) { @@ -156,18 +175,18 @@ private String sanitizeName(@Nullable String name) { } @Nullable - private String sanitizeContentPattern(@Nullable String contentPattern) { - if (contentPattern == null) { - return contentPattern; + private String sanitizePattern(@Nullable String pattern) { + if (pattern == null) { + return pattern; } - contentPattern = contentPattern.trim(); + pattern = pattern.trim(); - if (contentPattern.isEmpty()) { + if (pattern.isEmpty()) { return null; } - return contentPattern; + return pattern; } private static final String DEFAULT_NAME_PREFIX = LicenseHeaderStep.class.getName(); @@ -195,15 +214,19 @@ private static class Runtime implements Serializable { private static final long serialVersionUID = 1475199492829130965L; private final Pattern delimiterPattern; + private final @Nullable Pattern skipLinesMatching; private final String yearSepOrFull; private final @Nullable String yearToday; private final @Nullable String beforeYear; private final @Nullable String afterYear; private final boolean updateYearWithLatest; private final boolean licenseHeaderWithRange; + private final boolean hasFileToken; + + private static final Pattern FILENAME_PATTERN = Pattern.compile("\\$FILE"); /** The license that we'd like enforced. */ - private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest) { + private Runtime(String licenseHeader, String delimiter, String yearSeparator, boolean updateYearWithLatest, @Nullable String skipLinesMatching) { if (delimiter.contains("\n")) { throw new IllegalArgumentException("The delimiter must not contain any newlines."); } @@ -213,6 +236,8 @@ private Runtime(String licenseHeader, String delimiter, String yearSeparator, bo licenseHeader = licenseHeader + "\n"; } this.delimiterPattern = Pattern.compile('^' + delimiter, Pattern.UNIX_LINES | Pattern.MULTILINE); + this.skipLinesMatching = skipLinesMatching == null ? null : Pattern.compile(skipLinesMatching); + this.hasFileToken = FILENAME_PATTERN.matcher(licenseHeader).find(); Optional yearToken = getYearToken(licenseHeader); if (yearToken.isPresent()) { @@ -253,7 +278,38 @@ private static Optional getYearToken(String licenseHeader) { } /** Formats the given string. */ - private String format(String raw) { + private String format(String raw, File file) { + if (skipLinesMatching == null) { + return addOrUpdateLicenseHeader(raw, file); + } else { + String[] lines = raw.split("\n"); + StringBuilder skippedLinesBuilder = new StringBuilder(); + StringBuilder remainingLinesBuilder = new StringBuilder(); + boolean lastMatched = true; + for (String line : lines) { + if (lastMatched) { + Matcher matcher = skipLinesMatching.matcher(line); + if (matcher.find()) { + skippedLinesBuilder.append(line).append('\n'); + } else { + remainingLinesBuilder.append(line).append('\n'); + lastMatched = false; + } + } else { + remainingLinesBuilder.append(line).append('\n'); + } + } + return skippedLinesBuilder + addOrUpdateLicenseHeader(remainingLinesBuilder.toString(), file); + } + } + + private String addOrUpdateLicenseHeader(String raw, File file) { + raw = replaceYear(raw); + raw = replaceFileName(raw, file); + return raw; + } + + private String replaceYear(String raw) { Matcher contentMatcher = delimiterPattern.matcher(raw); if (!contentMatcher.find()) { throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern); @@ -348,7 +404,7 @@ private String calculateYearBySearching(String content) { } } } else { - System.err.println("Can't parse copyright year '" + content + "', defaulting to " + yearToday); + LOGGER.warn("Can't parse copyright year '{}', defaulting to {}", content, yearToday); // couldn't recognize the year format return yearToday; } @@ -383,6 +439,19 @@ private String setLicenseHeaderYearsFromGitHistory(String raw, File file) throws return beforeYear + yearRange + afterYear + raw.substring(contentMatcher.start()); } + private String replaceFileName(String raw, File file) { + if (!hasFileToken) { + return raw; + } + Matcher contentMatcher = delimiterPattern.matcher(raw); + if (!contentMatcher.find()) { + throw new IllegalArgumentException("Unable to find delimiter regex " + delimiterPattern); + } + String header = raw.substring(0, contentMatcher.start()); + String content = raw.substring(contentMatcher.start()); + return FILENAME_PATTERN.matcher(header).replaceAll(file.getName()) + content; + } + private static String parseYear(String cmd, File file) throws IOException { String fullCmd = cmd + " -- " + file.getAbsolutePath(); ProcessBuilder builder = new ProcessBuilder().directory(file.getParentFile()); diff --git a/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java b/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java index 31b30fe56f..3e58c9dbae 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,24 +35,37 @@ private NativeCmdStep() {} public static FormatterStep create(String name, File pathToExe, List arguments) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(pathToExe, "pathToExe"); - return FormatterStep.createLazy(name, () -> new State(FileSignature.signAsList(pathToExe), arguments), State::toFunc); + return FormatterStep.createLazy(name, () -> new State(FileSignature.promise(pathToExe), arguments), State::toRuntime, Runtime::toFunc); } static class State implements Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; + final FileSignature.Promised pathToExe; + final List arguments; - final FileSignature pathToExe; + State(FileSignature.Promised pathToExe, List arguments) { + this.pathToExe = pathToExe; + this.arguments = arguments; + } + + Runtime toRuntime() { + return new Runtime(pathToExe.get().getOnlyFile(), arguments); + } + } + static class Runtime implements Serializable { + private static final long serialVersionUID = 2L; + final File pathToExe; final List arguments; - State(FileSignature pathToExe, List arguments) { + Runtime(File pathToExe, List arguments) { this.pathToExe = pathToExe; this.arguments = arguments; } String format(ProcessRunner runner, String input) throws IOException, InterruptedException { List argumentsWithPathToExe = new ArrayList<>(); - argumentsWithPathToExe.add(pathToExe.getOnlyFile().getAbsolutePath()); + argumentsWithPathToExe.add(pathToExe.getAbsolutePath()); if (arguments != null) { argumentsWithPathToExe.addAll(arguments); } diff --git a/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java b/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java deleted file mode 100644 index 38373fec59..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2020-2021 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.generic; - -import java.io.File; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.FormatterFunc; -import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.LineEnding; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -public class PipeStepPair { - /** The two steps will be named {@code In} and {@code Out}. */ - public static Builder named(String name) { - return new Builder(name); - } - - public static String defaultToggleName() { - return "toggle"; - } - - public static String defaultToggleOff() { - return "spotless:off"; - } - - public static String defaultToggleOn() { - return "spotless:on"; - } - - public static class Builder { - String name; - Pattern regex; - - private Builder(String name) { - this.name = Objects.requireNonNull(name); - } - - /** Defines the opening and closing markers. */ - public Builder openClose(String open, String close) { - return regex(Pattern.quote(open) + "([\\s\\S]*?)" + Pattern.quote(close)); - } - - /** Defines the pipe via regex. Must have *exactly one* capturing group. */ - public Builder regex(String regex) { - return regex(Pattern.compile(regex)); - } - - /** Defines the pipe via regex. Must have *exactly one* capturing group. */ - public Builder regex(Pattern regex) { - this.regex = Objects.requireNonNull(regex); - return this; - } - - /** Returns a pair of steps which captures in the first part, then returns in the second. */ - public PipeStepPair buildPair() { - return new PipeStepPair(name, regex); - } - - /** Returns a single step which will apply the given steps only within the blocks selected by the regex / openClose pair. */ - public FormatterStep buildStepWhichAppliesSubSteps(Path rootPath, Collection steps) { - return FormatterStep.createLazy(name, - () -> new StateApplyToBlock(regex, steps), - state -> FormatterFunc.Closeable.of(state.buildFormatter(rootPath), state::format)); - } - } - - final FormatterStep in, out; - - private PipeStepPair(String name, Pattern pattern) { - StateIn stateIn = new StateIn(pattern); - StateOut stateOut = new StateOut(stateIn); - in = FormatterStep.create(name + "In", stateIn, state -> state::format); - out = FormatterStep.create(name + "Out", stateOut, state -> state::format); - } - - public FormatterStep in() { - return in; - } - - public FormatterStep out() { - return out; - } - - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - static class StateApplyToBlock extends StateIn implements Serializable { - private static final long serialVersionUID = -844178006407733370L; - - final List steps; - final transient StringBuilder builder = new StringBuilder(); - - StateApplyToBlock(Pattern regex, Collection steps) { - super(regex); - this.steps = new ArrayList<>(steps); - } - - Formatter buildFormatter(Path rootDir) { - return Formatter.builder() - .encoding(StandardCharsets.UTF_8) // can be any UTF, doesn't matter - .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) // just internal, won't conflict with user - .steps(steps) - .rootDir(rootDir) - .build(); - } - - private String format(Formatter formatter, String unix, File file) throws Exception { - groups.clear(); - Matcher matcher = regex.matcher(unix); - while (matcher.find()) { - // apply the formatter to each group - groups.add(formatter.compute(matcher.group(1), file)); - } - // and then assemble the result right away - return stateOutCompute(this, builder, unix); - } - } - - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - static class StateIn implements Serializable { - private static final long serialVersionUID = -844178006407733370L; - - final Pattern regex; - - public StateIn(Pattern regex) { - this.regex = Objects.requireNonNull(regex); - } - - final transient ArrayList groups = new ArrayList<>(); - - private String format(String unix) throws Exception { - groups.clear(); - Matcher matcher = regex.matcher(unix); - while (matcher.find()) { - groups.add(matcher.group(1)); - } - return unix; - } - } - - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - static class StateOut implements Serializable { - private static final long serialVersionUID = -1195263184715054229L; - - final StateIn in; - - StateOut(StateIn in) { - this.in = Objects.requireNonNull(in); - } - - final transient StringBuilder builder = new StringBuilder(); - - private String format(String unix) { - return stateOutCompute(in, builder, unix); - } - } - - private static String stateOutCompute(StateIn in, StringBuilder builder, String unix) { - if (in.groups.isEmpty()) { - return unix; - } - builder.setLength(0); - Matcher matcher = in.regex.matcher(unix); - int lastEnd = 0; - int groupIdx = 0; - while (matcher.find()) { - builder.append(unix, lastEnd, matcher.start(1)); - builder.append(in.groups.get(groupIdx)); - lastEnd = matcher.end(1); - ++groupIdx; - } - if (groupIdx == in.groups.size()) { - builder.append(unix, lastEnd, unix.length()); - return builder.toString(); - } else { - // throw an error with either the full regex, or the nicer open/close pair - Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E") - .matcher(in.regex.pattern()); - String pattern; - if (openClose.matches()) { - pattern = openClose.group(1) + " " + openClose.group(2); - } else { - pattern = in.regex.pattern(); - } - throw new Error("An intermediate step removed a match of " + pattern); - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsConfig.java b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsConfig.java new file mode 100644 index 0000000000..d7962c6543 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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.gherkin; + +import java.io.Serializable; + +public class GherkinUtilsConfig implements Serializable { + private static final long serialVersionUID = 1L; + + public static int defaultIndentSpaces() { + // https://cucumber.io/docs/gherkin/reference/ + // Recommended indentation is 2 spaces + return 2; + } + + final int indentSpaces; + + public GherkinUtilsConfig(int indentSpaces) { + this.indentSpaces = indentSpaces; + } + + public int getIndentSpaces() { + return indentSpaces; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java new file mode 100644 index 0000000000..63cdd7a786 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/gherkin/GherkinUtilsStep.java @@ -0,0 +1,77 @@ +/* + * Copyright 2021-2024 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.gherkin; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class GherkinUtilsStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String MAVEN_COORDINATE = "io.cucumber:gherkin-utils:"; + private static final String DEFAULT_VERSION = "9.0.0"; + public static final String NAME = "gherkinUtils"; + + private final JarState.Promised jarState; + private final GherkinUtilsConfig gherkinSimpleConfig; + + private GherkinUtilsStep(JarState.Promised jarState, GherkinUtilsConfig gherkinSimpleConfig) { + this.jarState = jarState; + this.gherkinSimpleConfig = gherkinSimpleConfig; + } + + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + public static FormatterStep create(GherkinUtilsConfig gherkinSimpleConfig, + String formatterVersion, Provisioner provisioner) { + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new GherkinUtilsStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + formatterVersion, provisioner)), gherkinSimpleConfig), + GherkinUtilsStep::equalityState, + GherkinUtilsStep.State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get(), gherkinSimpleConfig); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final GherkinUtilsConfig gherkinSimpleConfig; + private final JarState jarState; + + State(JarState jarState, GherkinUtilsConfig gherkinSimpleConfig) { + this.jarState = jarState; + this.gherkinSimpleConfig = gherkinSimpleConfig; + } + + FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + InstantiationException, IllegalAccessException { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.gherkin.GherkinUtilsFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(GherkinUtilsConfig.class); + return (FormatterFunc) constructor.newInstance(gherkinSimpleConfig); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/go/GofmtFormatStep.java b/lib/src/main/java/com/diffplug/spotless/go/GofmtFormatStep.java new file mode 100644 index 0000000000..ffdc17cd21 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/go/GofmtFormatStep.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 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.go; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Note: gofmt doesn't have a version flag, because it's part of standard Go distribution. + * So `go` executable can be used to determine base path and version, and path to gofmt can be built from it. + */ +public class GofmtFormatStep { + public static String name() { + return "gofmt"; + } + + public static String defaultVersion() { + return "go1.20.0"; + } + + private final String version; + private final @Nullable String pathToExe; + + private GofmtFormatStep(String version, String pathToExe) { + this.version = version; + this.pathToExe = pathToExe; + } + + public static GofmtFormatStep withVersion(String version) { + return new GofmtFormatStep(version, null); + } + + public GofmtFormatStep withGoExecutable(String pathToExe) { + return new GofmtFormatStep(version, pathToExe); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createRountrip, RoundtripState::toEquality, EqualityState::toFunc); + } + + private RoundtripState createRountrip() throws IOException, InterruptedException { + String howToInstall = "gofmt is a part of standard go distribution. If spotless can't discover it automatically, " + + "you can point Spotless to the go binary with {@code pathToExe('/path/to/go')}"; + final ForeignExe exe = ForeignExe.nameAndVersion("go", version) + .pathToExe(pathToExe) + .versionFlag("version") + .fixCantFind(howToInstall) + .fixWrongVersion( + "You can tell Spotless to use the version you already have with {@code gofmt('{versionFound}')}" + + "or you can install the currently specified Go version, {version}.\n" + howToInstall); + return new RoundtripState(version, exe); + } + + static class RoundtripState implements Serializable { + private static final long serialVersionUID = 1L; + + final String version; + final ForeignExe exe; + + RoundtripState(String version, ForeignExe exe) { + this.version = version; + this.exe = exe; + } + + private EqualityState toEquality() { + return new EqualityState(version, exe); + } + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + static class EqualityState implements Serializable { + private static final long serialVersionUID = -1825662355363926318L; + // used for up-to-date checks and caching + final String version; + final transient ForeignExe exe; + + public EqualityState(String version, ForeignExe goExecutable) { + this.version = version; + this.exe = Objects.requireNonNull(goExecutable); + } + + String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + final List processArgs = new ArrayList<>(); + String pathToGoBinary = exe.confirmVersionAndGetAbsolutePath(); + Path goBasePath = Path.of(pathToGoBinary).getParent(); + if (goBasePath == null) { + throw new IllegalStateException("Unable to resolve base path of Go installation directory"); + } + String pathToGoFmt = goBasePath.resolve("gofmt").toString(); + processArgs.add(pathToGoFmt); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), processArgs).assertExitZero(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java b/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java new file mode 100644 index 0000000000..6dc3185ef6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/groovy/RemoveSemicolonsStep.java @@ -0,0 +1,77 @@ +/* + * Copyright 2023-2024 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.groovy; + +import java.io.BufferedReader; +import java.io.Serializable; +import java.io.StringReader; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; + +/** + * Removes all semicolons from the end of lines. + * + * @author Jose Luis Badano + */ +public final class RemoveSemicolonsStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String NAME = "Remove unnecessary semicolons"; + + private RemoveSemicolonsStep() { + // do not instantiate + } + + public static FormatterStep create() { + return FormatterStep.create(NAME, + new State(), + State::toFormatter); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + FormatterFunc toFormatter() { + return raw -> { + try (BufferedReader reader = new BufferedReader(new StringReader(raw))) { + StringBuilder result = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + result.append(removeSemicolon(line)); + result.append(System.lineSeparator()); + } + return result.toString(); + } + }; + } + + /** + * Removes the last semicolon in a line if it exists. + * + * @param line the line to remove the semicolon from + * @return the line without the last semicolon + */ + private String removeSemicolon(String line) { + // Find the last semicolon in a string and remove it. + int lastSemicolon = line.lastIndexOf(";"); + if (lastSemicolon != -1 && lastSemicolon == line.length() - 1) { + return line.substring(0, lastSemicolon); + } else { + return line; + } + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java b/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java new file mode 100644 index 0000000000..2762bbeaf9 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/CleanthatJavaStep.java @@ -0,0 +1,220 @@ +/* + * Copyright 2023-2024 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.File; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; + +import com.diffplug.spotless.Formatter; +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; + +/** + * Enables CleanThat as a SpotLess step. + * + * @author Benoit Lacelle + */ +// https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#how-to-add-a-new-formatterstep +public final class CleanthatJavaStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String NAME = "cleanthat"; + private static final String MAVEN_COORDINATE = "io.github.solven-eu.cleanthat:java"; + /** + * CleanThat changelog is available at here. + */ + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "2.22"); + + private final JarState.Promised jarState; + private final String version; + private final String sourceJdkVersion; + private final List included; + private final List excluded; + private final boolean includeDraft; + + private CleanthatJavaStep(JarState.Promised jarState, + String version, + String sourceJdkVersion, + List included, + List excluded, + boolean includeDraft) { + this.jarState = jarState; + this.version = version; + + this.sourceJdkVersion = sourceJdkVersion; + this.included = included; + this.excluded = excluded; + this.includeDraft = includeDraft; + } + + /** Creates a step that applies default CleanThat mutators. */ + public static FormatterStep create(Provisioner provisioner) { + return create(defaultVersion(), provisioner); + } + + /** Creates a step that applies default CleanThat mutators. */ + public static FormatterStep create(String version, Provisioner provisioner) { + return createWithStepName(NAME, MAVEN_COORDINATE, version, defaultSourceJdk(), defaultMutators(), defaultExcludedMutators(), defaultIncludeDraft(), provisioner); + } + + public static String defaultSourceJdk() { + // see IJdkVersionConstants.JDK_7 + // https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#source + // 1.7 is the default for 'maven-compiler-plugin' since 3.9.0 + return "1.7"; + } + + /** + * By default, we include only safe and consensual mutators + */ + public static List defaultMutators() { + // see ICleanthatStepParametersProperties.SAFE_AND_CONSENSUAL + return List.of("SafeAndConsensual"); + } + + public static List defaultExcludedMutators() { + return List.of(); + } + + public static boolean defaultIncludeDraft() { + return false; + } + + /** Creates a step that applies selected CleanThat mutators. */ + static FormatterStep createWithStepName(String stepName, + String groupArtifact, + String version, + String sourceJdkVersion, + List included, + List excluded, + boolean includeDraft, + Provisioner provisioner) { + Objects.requireNonNull(groupArtifact, "groupArtifact"); + if (groupArtifact.chars().filter(ch -> ch == ':').count() != 1) { + throw new IllegalArgumentException("groupArtifact must be in the form 'groupId:artifactId'. it was: " + groupArtifact); + } + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(provisioner, "provisioner"); + return FormatterStep.create(stepName, + new CleanthatJavaStep(JarState.promise(() -> JarState.from(groupArtifact + ":" + version, provisioner)), version, sourceJdkVersion, included, excluded, includeDraft), + CleanthatJavaStep::equalityState, + State::createFormat); + } + + /** Creates a step that applies selected CleanThat mutators. */ + public static FormatterStep create(String groupArtifact, + String version, + String sourceJdkVersion, + List included, + List excluded, + boolean includeDraft, + Provisioner provisioner) { + return createWithStepName(NAME, groupArtifact, version, sourceJdkVersion, included, excluded, includeDraft, provisioner); + } + + /** Get default formatter version */ + public static String defaultVersion() { + return Objects.requireNonNull(JVM_SUPPORT.getRecommendedFormatterVersion()); + } + + public static String defaultGroupArtifact() { + return MAVEN_COORDINATE; + } + + private State equalityState() { + return new State(jarState.get(), version, sourceJdkVersion, included, excluded, includeDraft); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final JarState jarState; + private final String version; + private final String sourceJdkVersion; + private final List included; + private final List excluded; + private final boolean includeDraft; + + State(JarState jarState, + String version, + String sourceJdkVersion, + List included, + List excluded, + boolean includeDraft) { + JVM_SUPPORT.assertFormatterSupported(version); + ModuleHelper.doOpenInternalPackagesIfRequired(); + this.jarState = jarState; + this.version = version; + this.sourceJdkVersion = sourceJdkVersion; + this.included = included; + this.excluded = excluded; + this.includeDraft = includeDraft; + } + + private static class JvmSupportFormatterFunc implements FormatterFunc { + + final Object formatter; + final Method formatterMethod; + + private JvmSupportFormatterFunc(Object formatter, Method formatterMethod) { + this.formatter = formatter; + this.formatterMethod = formatterMethod; + } + + @Override + public String apply(String input) throws Exception { + return apply(input, Formatter.NO_FILE_SENTINEL); + } + + @Override + public String apply(String input, File file) throws Exception { + if (file.isAbsolute()) { + // Cleanthat expects a relative file as input (relative to the root of the repository) + Path absolutePath = file.toPath(); + file = absolutePath.subpath(1, absolutePath.getNameCount()).toFile(); + } + return (String) formatterMethod.invoke(formatter, input, file); + } + } + + FormatterFunc createFormat() { + ClassLoader classLoader = jarState.getClassLoader(); + + Object formatter; + Method formatterMethod; + try { + Class formatterClazz = classLoader.loadClass("com.diffplug.spotless.glue.java.JavaCleanthatRefactorerFunc"); + Constructor formatterConstructor = formatterClazz.getConstructor(String.class, List.class, List.class, boolean.class); + + formatter = formatterConstructor.newInstance(sourceJdkVersion, included, excluded, includeDraft); + formatterMethod = formatterClazz.getMethod("apply", String.class, File.class); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Issue executing the formatter", e); + } + + FormatterFunc formatterFunc = new JvmSupportFormatterFunc(formatter, formatterMethod); + + return JVM_SUPPORT.suggestLaterVersionOnError(version, formatterFunc); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java b/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java new file mode 100644 index 0000000000..998c147ef5 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java @@ -0,0 +1,505 @@ +/* + * Copyright 2022-2024 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.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.SerializedFunction; + +/** + * Some formatters put every annotation on its own line + * -- even type annotations, which should be on the same line as the type they qualify. + * This class corrects the formatting. + * This is useful as a postprocessing step after a Java formatter that is not cognizant of type annotations. + + *

+ * Note: A type annotation is an annotation that is meta-annotated with {@code @Target({ElementType.TYPE_USE})}. + */ +public final class FormatAnnotationsStep implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * Simple names of type annotations. + * A type annotation is an annotation that is meta-annotated with @Target({ElementType.TYPE_USE}). + * A type annotation should be formatted on the same line as the type it qualifies. + */ + private static final List defaultTypeAnnotations = + // Use simple names because Spotless has no access to the + // fully-qualified names or the definitions of the type qualifiers. + Arrays.asList( + // Type annotations from the Checker Framework and all + // the tools it supports: FindBugs, JetBrains (IntelliJ), + // Eclipse, NetBeans, Spring, JML, Android, etc. + "A", + "ACCBottom", + "Acceleration", + "ACCTop", + "AinferBottom", + "AinferDefaultType", + "AinferParent", + "AinferSibling1", + "AinferSibling2", + "AinferTop", + "AinferImplicitAnno", + "AinferSiblingWithFields", + "AlwaysSafe", + "Angle", + "AnnoWithStringArg", + "Area", + "ArrayLen", + "ArrayLenRange", + "ArrayWithoutPackage", + "AssertFalse", + "AssertTrue", + "AwtAlphaCompositingRule", + "AwtColorSpace", + "AwtCursorType", + "AwtFlowLayout", + "B", + "BinaryName", + "BinaryNameInUnnamedPackage", + "BinaryNameOrPrimitiveType", + "BinaryNameWithoutPackage", + "BoolVal", + "Bottom", + "BottomQualifier", + "BottomThis", + "BottomVal", + "C", + "CalledMethods", + "CalledMethodsBottom", + "CalledMethodsPredicate", + "CalledMethodsTop", + "CanonicalName", + "CanonicalNameAndBinaryName", + "CanonicalNameOrEmpty", + "CanonicalNameOrPrimitiveType", + "CCBottom", + "CCTop", + "cd", + "ClassBound", + "ClassGetName", + "ClassGetSimpleName", + "ClassVal", + "ClassValBottom", + "CompilerMessageKey", + "CompilerMessageKeyBottom", + "Constant", + "Critical", + "Current", + "D", + "DecimalMax", + "DecimalMin", + "DefaultType", + "degrees", + "Det", + "Digits", + "DoesNotMatchRegex", + "DotSeparatedIdentifiers", + "DotSeparatedIdentifiersOrPrimitiveType", + "DoubleVal", + "E", + "Email", + "Encrypted", + "EnhancedRegex", + "EnumVal", + "Even", + "F", + "FBCBottom", + "FEBottom", + "FEBot", + "Fenum", + "FenumBottom", + "FenumTop", + "FETop", + "FieldDescriptor", + "FieldDescriptorForPrimitive", + "FieldDescriptorForPrimitiveOrArrayInUnnamedPackage", + "FieldDescriptorWithoutPackage", + "FlowExp", + "Force", + "Format", + "FormatBottom", + "FqBinaryName", + "Frequency", + "FullyQualifiedName", + "Future", + "FutureOrPresent", + "g", + "GTENegativeOne", + "GuardedBy", + "GuardedByBottom", + "GuardedByUnknown", + "GuardSatisfied", + "h", + "H1Bot", + "H1Invalid", + "H1Poly", + "H1S1", + "H1S2", + "H1Top", + "H2Bot", + "H2Poly", + "H2S1", + "H2S2", + "H2Top", + "Hz", + "I18nFormat", + "I18nFormatBottom", + "I18nFormatFor", + "I18nInvalidFormat", + "I18nUnknownFormat", + "Identifier", + "IdentifierOrArray", + "IdentifierOrPrimitiveType", + "ImplicitAnno", + "IndexFor", + "IndexOrHigh", + "IndexOrLow", + "Initialized", + "InitializedFields", + "InitializedFieldsBottom", + "InitializedFieldsPredicate", + "InternalForm", + "Interned", + "InternedDistinct", + "IntRange", + "IntVal", + "InvalidFormat", + "K", + "KeyFor", + "KeyForBottom", + "KeyForType", + "kg", + "kHz", + "km", + "km2", + "km3", + "kmPERh", + "kN", + "LbTop", + "LB_TOP", + "LeakedToResult", + "Length", + "LengthOf", + "LessThan", + "LessThanBottom", + "LessThanUnknown", + "LocalizableKey", + "LocalizableKeyBottom", + "Localized", + "LowerBoundBottom", + "LowerBoundUnknown", + "LTEqLengthOf", + "LTLengthOf", + "LTOMLengthOf", + "Luminance", + "m", + "m2", + "m3", + "Mass", + "MatchesRegex", + "Max", + "MaybeAliased", + "MaybeDerivedFromConstant", + "MaybePresent", + "MaybeThis", + "MethodDescriptor", + "MethodVal", + "MethodValBottom", + "min", + "Min", + "MinLen", + "mm", + "mm2", + "mm3", + "mol", + "MonotonicNonNull", + "MonotonicNonNullType", + "MonotonicOdd", + "mPERs", + "mPERs2", + "MustCall", + "MustCallAlias", + "MustCallUnknown", + "N", + "Negative", + "NegativeIndexFor", + "NegativeOrZero", + "NewObject", + "NonConstant", + "NonDet", + "NonLeaked", + "NonNegative", + "NonNull", + "NonNullType", + "NonRaw", + "NotBlank", + "NotCalledMethods", + "NotEmpty", + "NotNull", + "NotQualifier", + "NTDBottom", + "NTDMiddle", + "NTDSide", + "NTDTop", + "Null", + "Nullable", + "NullableType", + "Odd", + "OptionalBottom", + "OrderNonDet", + "Parent", + "Past", + "PastOrPresent", + "Pattern", + "PatternA", + "PatternAB", + "PatternAC", + "PatternB", + "PatternBC", + "PatternBottomFull", + "PatternBottomPartial", + "PatternC", + "PatternUnknown", + "Poly", + "PolyAll", + "PolyConstant", + "PolyDet", + "PolyEncrypted", + "PolyFenum", + "PolyIndex", + "PolyInitializedFields", + "PolyInterned", + "PolyKeyFor", + "PolyLength", + "PolyLowerBound", + "PolyMustCall", + "PolyNull", + "PolyNullType", + "PolyPresent", + "PolyRaw", + "PolyReflection", + "PolyRegex", + "PolySameLen", + "PolySignature", + "PolySigned", + "PolyTainted", + "PolyTestAccumulation", + "PolyTypeDeclDefault", + "PolyUI", + "PolyUnit", + "PolyUpperBound", + "PolyValue", + "PolyVariableNameDefault", + "Positive", + "PositiveOrZero", + "Present", + "PrimitiveType", + "PropertyKey", + "PropertyKeyBottom", + "PurityUnqualified", + "Qualifier", + "radians", + "Raw", + "ReflectBottom", + "Regex", + "RegexBottom", + "RegexNNGroups", + "ReportUnqualified", + "s", + "SameLen", + "SameLenBottom", + "SameLenUnknown", + "SearchIndexBottom", + "SearchIndexFor", + "SearchIndexUnknown", + "Sibling1", + "Sibling2", + "SiblingWithFields", + "SignatureBottom", + "Signed", + "SignednessBottom", + "SignednessGlb", + "SignedPositive", + "SignedPositiveFromUnsigned", + "Size", + "Speed", + "StringVal", + "SubQual", + "Substance", + "SubstringIndexBottom", + "SubstringIndexFor", + "SubstringIndexUnknown", + "SuperQual", + "SwingBoxOrientation", + "SwingCompassDirection", + "SwingElementOrientation", + "SwingHorizontalOrientation", + "SwingSplitPaneOrientation", + "SwingTextOrientation", + "SwingTitleJustification", + "SwingTitlePosition", + "SwingVerticalOrientation", + "t", + "Tainted", + "Temperature", + "TestAccumulation", + "TestAccumulationBottom", + "TestAccumulationPredicate", + "This", + "Time", + "Top", + "TypeDeclDefaultBottom", + "TypeDeclDefaultMiddle", + "TypeDeclDefaultTop", + "UbTop", + "UB_TOP", + "UI", + "UnderInitialization", + "Unique", + "UnitsBottom", + "UnknownClass", + "UnknownCompilerMessageKey", + "UnknownFormat", + "UnknownInitialization", + "UnknownInterned", + "UnknownKeyFor", + "UnknownLocalizableKey", + "UnknownLocalized", + "UnknownMethod", + "UnknownPropertyKey", + "UnknownRegex", + "UnknownSignedness", + "UnknownThis", + "UnknownUnits", + "UnknownVal", + "Unsigned", + "Untainted", + "UpperBoundBottom", + "UpperBoundLiteral", + "UpperBoundUnknown", + "Valid", + "ValueTypeAnno", + "VariableNameDefaultBottom", + "VariableNameDefaultMiddle", + "VariableNameDefaultTop", + "Volume", + "WholeProgramInferenceBottom" + // TODO: Add type annotations from other tools here. + + ); + + private static final String NAME = "No line break between type annotation and type"; + + public static FormatterStep create() { + return create(Collections.emptyList(), Collections.emptyList()); + } + + public static FormatterStep create(List addedTypeAnnotations, List removedTypeAnnotations) { + return FormatterStep.create(NAME, new State(addedTypeAnnotations, removedTypeAnnotations), SerializedFunction.identity(), State::toFormatter); + } + + private FormatAnnotationsStep() {} + + // TODO: Read from a local .type-annotations file. + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final Set typeAnnotations = new HashSet<>(defaultTypeAnnotations); + + // group 1 is the basename of the annotation. + private static final String annoNoArgRegex = "@(?:[A-Za-z_][A-Za-z0-9_.]*\\.)?([A-Za-z_][A-Za-z0-9_]*)"; + private static final Pattern annoNoArgPattern = Pattern.compile(annoNoArgRegex); + // 3 non-empty cases: () (".*") (.*) + private static final String annoArgRegex = "(?:\\(\\)|\\(\"[^\"]*\"\\)|\\([^\")][^)]*\\))?"; + // group 1 is the basename of the annotation. + private static final String annoRegex = annoNoArgRegex + annoArgRegex; + private static final String trailingAnnoRegex = annoRegex + "$"; + private static final Pattern trailingAnnoPattern = Pattern.compile(trailingAnnoRegex); + + // Heuristic: matches if the line might be within a //, /*, or Javadoc comment. + private static final Pattern withinCommentPattern = Pattern.compile("//|/\\*(?!.*/*/)|^[ \t]*\\*[ \t]"); + // Don't move an annotation to the start of a comment line. + private static final Pattern startsWithCommentPattern = Pattern.compile("^[ \t]*(//|/\\*$|/\\*|void\\b)"); + + /** + * @param addedTypeAnnotations simple names to add to Spotless's default list + * @param removedTypeAnnotations simple names to remove from Spotless's default list + */ + State(List addedTypeAnnotations, List removedTypeAnnotations) { + typeAnnotations.addAll(addedTypeAnnotations); + typeAnnotations.removeAll(removedTypeAnnotations); + } + + FormatterFunc toFormatter() { + return this::fixupTypeAnnotations; + } + + /** + * Removes line break between type annotations and the following type. + * + * @param unixStr the text of a Java file + * @return corrected text of the Java file + */ + String fixupTypeAnnotations(String unixStr) { + // Each element of `lines` ends with a newline. + String[] lines = unixStr.split("((?<=\n))"); + for (int i = 0; i < lines.length - 1; i++) { + String line = lines[i]; + if (endsWithTypeAnnotation(line)) { + String nextLine = lines[i + 1]; + if (startsWithCommentPattern.matcher(nextLine).find()) { + continue; + } + lines[i] = ""; + lines[i + 1] = line.replaceAll("\\s+$", "") + " " + nextLine.replaceAll("^\\s+", ""); + } + } + return String.join("", lines); + } + + /** + * Returns true if the line ends with a type annotation. + * FormatAnnotationsStep fixes such formatting. + */ + boolean endsWithTypeAnnotation(String unixLine) { + // Remove trailing newline. + String line = unixLine.replaceAll("\\s+$", ""); + Matcher m = trailingAnnoPattern.matcher(line); + if (!m.find()) { + return false; + } + String preceding = line.substring(0, m.start()); + String basename = m.group(1); + + if (withinCommentPattern.matcher(preceding).find()) { + return false; + } + + return typeAnnotations.contains(basename); + } + } +} 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 c5ef2e576a..5d63952e2c 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-2022 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,48 +16,46 @@ package com.diffplug.spotless.java; import java.io.Serializable; -import java.lang.reflect.Method; +import java.lang.reflect.Constructor; import java.util.Objects; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.JarState; import com.diffplug.spotless.Jvm; -import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.ThrowingEx.BiFunction; -import com.diffplug.spotless.ThrowingEx.Function; /** Wraps up google-java-format as a FormatterStep. */ -public class GoogleJavaFormatStep { - // prevent direct instantiation - private GoogleJavaFormatStep() {} - +public class GoogleJavaFormatStep implements Serializable { + private static final long serialVersionUID = 1L; private static final String DEFAULT_STYLE = "GOOGLE"; private static final boolean DEFAULT_REFLOW_LONG_STRINGS = false; - static final String NAME = "google-java-format"; - static final String MAVEN_COORDINATE = "com.google.googlejavaformat:google-java-format"; - static final String FORMATTER_CLASS = "com.google.googlejavaformat.java.Formatter"; - static final String FORMATTER_METHOD = "formatSource"; - - private static final String OPTIONS_CLASS = "com.google.googlejavaformat.java.JavaFormatterOptions"; - private static final String OPTIONS_BUILDER_METHOD = "builder"; - private static final String OPTIONS_BUILDER_CLASS = "com.google.googlejavaformat.java.JavaFormatterOptions$Builder"; - private static final String OPTIONS_BUILDER_STYLE_METHOD = "style"; - private static final String OPTIONS_BUILDER_BUILD_METHOD = "build"; - private static final String OPTIONS_Style = "com.google.googlejavaformat.java.JavaFormatterOptions$Style"; - - private static final String REMOVE_UNUSED_CLASS = "com.google.googlejavaformat.java.RemoveUnusedImports"; - private static final String REMOVE_UNUSED_METHOD = "removeUnusedImports"; - - private static final String REMOVE_UNUSED_IMPORT_JavadocOnlyImports = "com.google.googlejavaformat.java.RemoveUnusedImports$JavadocOnlyImports"; - private static final String REMOVE_UNUSED_IMPORT_JavadocOnlyImports_Keep = "KEEP"; - - private static final String IMPORT_ORDERER_CLASS = "com.google.googlejavaformat.java.ImportOrderer"; - private static final String IMPORT_ORDERER_METHOD = "reorderImports"; - - private static final String STRING_WRAPPER_CLASS = "com.google.googlejavaformat.java.StringWrapper"; - private static final String STRING_WRAPPER_METHOD = "wrap"; + private static final boolean DEFAULT_REORDER_IMPORTS = false; + private static final boolean DEFAULT_FORMAT_JAVADOC = true; + private static final String NAME = "google-java-format"; + private static final String MAVEN_COORDINATE = "com.google.googlejavaformat:google-java-format"; + + /** The jar that contains the formatter. */ + private final JarState.Promised jarState; + private final String version; + private final String style; + private final boolean reflowLongStrings; + private final boolean reorderImports; + private final boolean formatJavadoc; + + private GoogleJavaFormatStep(JarState.Promised jarState, + String version, + String style, + boolean reflowLongStrings, + boolean reorderImports, + boolean formatJavadoc) { + this.jarState = jarState; + this.version = version; + this.style = style; + this.reflowLongStrings = reflowLongStrings; + this.reorderImports = reorderImports; + this.formatJavadoc = formatJavadoc; + } /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(Provisioner provisioner) { @@ -79,8 +77,24 @@ public static FormatterStep create(String version, String style, Provisioner pro return create(MAVEN_COORDINATE, version, style, provisioner, reflowLongStrings); } - /** Creates a step which formats everything - groupArtifact, code, import order, and unused imports - and optionally reflows long strings. */ public static FormatterStep create(String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings) { + return create(groupArtifact, version, style, provisioner, reflowLongStrings, DEFAULT_REORDER_IMPORTS); + } + + public static FormatterStep create(String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports) { + return create(groupArtifact, version, style, provisioner, reflowLongStrings, reorderImports, DEFAULT_FORMAT_JAVADOC); + } + + /** Creates a step which formats everything - groupArtifact, code, import order, and unused imports - and optionally reflows long strings. */ + public static FormatterStep create(String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc) { + return createInternally(NAME, groupArtifact, version, style, provisioner, reflowLongStrings, reorderImports, formatJavadoc, false); + } + + static FormatterStep createRemoveUnusedImportsOnly(Provisioner provisioner) { + return createInternally(RemoveUnusedImportsStep.NAME, MAVEN_COORDINATE, defaultVersion(), defaultStyle(), provisioner, defaultReflowLongStrings(), defaultReorderImports(), defaultFormatJavadoc(), true); + } + + private static FormatterStep createInternally(String name, String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc, boolean removeImports) { Objects.requireNonNull(groupArtifact, "groupArtifact"); if (groupArtifact.chars().filter(ch -> ch == ':').count() != 1) { throw new IllegalArgumentException("groupArtifact must be in the form 'groupId:artifactId'"); @@ -88,12 +102,26 @@ public static FormatterStep create(String groupArtifact, String version, String Objects.requireNonNull(version, "version"); Objects.requireNonNull(style, "style"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(NAME, groupArtifact, version, style, provisioner, reflowLongStrings), - State::createFormat); + + GoogleJavaFormatStep step = new GoogleJavaFormatStep(JarState.promise(() -> JarState.from(groupArtifact + ":" + version, provisioner)), version, style, reflowLongStrings, reorderImports, formatJavadoc); + if (removeImports) { + return FormatterStep.create(name, + step, + GoogleJavaFormatStep::equalityState, + State::createRemoveUnusedImportsOnly); + } else { + return FormatterStep.create(name, + step, + GoogleJavaFormatStep::equalityState, + State::createFormat); + } } - static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "1.7").add(11, "1.13.0"); + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME) + .addMin(11, "1.8") // we only support google-java-format >= 1.8 due to api changes + .addMin(16, "1.10.0") // java 16 requires at least 1.10.0 due to jdk api changes in JavaTokenizer + .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(11, "1.24.0"); // default version public static String defaultGroupArtifact() { return MAVEN_COORDINATE; @@ -101,7 +129,7 @@ public static String defaultGroupArtifact() { /** Get default formatter version */ public static String defaultVersion() { - return JVM_SUPPORT.getRecommendedFormatterVersion(); + return Objects.requireNonNull(JVM_SUPPORT.getRecommendedFormatterVersion()); } public static String defaultStyle() { @@ -112,151 +140,60 @@ public static boolean defaultReflowLongStrings() { return DEFAULT_REFLOW_LONG_STRINGS; } - static final class State implements Serializable { - private static final long serialVersionUID = 1L; - - /** The jar that contains the formatter. */ - final JarState jarState; - final String stepName; - final String version; - final String style; - final boolean reflowLongStrings; + public static boolean defaultReorderImports() { + return DEFAULT_REORDER_IMPORTS; + } - State(String stepName, String version, Provisioner provisioner) throws Exception { - this(stepName, version, DEFAULT_STYLE, provisioner); - } + public static boolean defaultFormatJavadoc() { + return DEFAULT_FORMAT_JAVADOC; + } - State(String stepName, String version, String style, Provisioner provisioner) throws Exception { - this(stepName, version, style, provisioner, DEFAULT_REFLOW_LONG_STRINGS); - } + private State equalityState() { + return new State(version, style, jarState.get(), reflowLongStrings, reorderImports, formatJavadoc); + } - State(String stepName, String version, String style, Provisioner provisioner, boolean reflowLongStrings) throws Exception { - this(stepName, MAVEN_COORDINATE, version, style, provisioner, reflowLongStrings); - } + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; - State(String stepName, String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings) throws Exception { + private final JarState jarState; + private final String version; + private final String style; + private final boolean reflowLongStrings; + private final boolean reorderImports; + private final boolean formatJavadoc; + + State(String version, + String style, + JarState jarState, + boolean reflowLongStrings, + boolean reorderImports, + boolean formatJavadoc) { JVM_SUPPORT.assertFormatterSupported(version); - this.jarState = JarState.from(groupArtifact + ":" + version, provisioner); - this.stepName = stepName; + ModuleHelper.doOpenInternalPackagesIfRequired(); + this.jarState = jarState; this.version = version; this.style = style; this.reflowLongStrings = reflowLongStrings; + this.reorderImports = reorderImports; + this.formatJavadoc = formatJavadoc; } - @SuppressWarnings({"unchecked", "rawtypes"}) FormatterFunc createFormat() throws Exception { - ClassLoader classLoader = jarState.getClassLoader(); - - // instantiate the formatter and get its format method - Class optionsClass = classLoader.loadClass(OPTIONS_CLASS); - Class optionsBuilderClass = classLoader.loadClass(OPTIONS_BUILDER_CLASS); - Method optionsBuilderMethod = optionsClass.getMethod(OPTIONS_BUILDER_METHOD); - Object optionsBuilder = optionsBuilderMethod.invoke(null); - - Class optionsStyleClass = classLoader.loadClass(OPTIONS_Style); - Object styleConstant = Enum.valueOf((Class) optionsStyleClass, style); - Method optionsBuilderStyleMethod = optionsBuilderClass.getMethod(OPTIONS_BUILDER_STYLE_METHOD, optionsStyleClass); - optionsBuilderStyleMethod.invoke(optionsBuilder, styleConstant); - - Method optionsBuilderBuildMethod = optionsBuilderClass.getMethod(OPTIONS_BUILDER_BUILD_METHOD); - Object options = optionsBuilderBuildMethod.invoke(optionsBuilder); - - Class formatterClazz = classLoader.loadClass(FORMATTER_CLASS); - Object formatter = formatterClazz.getConstructor(optionsClass).newInstance(options); - Method formatterMethod = formatterClazz.getMethod(FORMATTER_METHOD, String.class); + final ClassLoader classLoader = jarState.getClassLoader(); + Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.java.GoogleJavaFormatFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(String.class, String.class, boolean.class, boolean.class, boolean.class); + FormatterFunc googleJavaFormatFormatterFunc = (FormatterFunc) constructor.newInstance(version, style, reflowLongStrings, reorderImports, formatJavadoc); - Function removeUnused = constructRemoveUnusedFunction(classLoader); - - Class importOrdererClass = classLoader.loadClass(IMPORT_ORDERER_CLASS); - Method importOrdererMethod = importOrdererClass.getMethod(IMPORT_ORDERER_METHOD, String.class); - - BiFunction reflowLongStrings = this.reflowLongStrings ? constructReflowLongStringsFunction(classLoader, formatterClazz) : (s, f) -> s; - - return JVM_SUPPORT.suggestLaterVersionOnError(version, (input -> { - String formatted = (String) formatterMethod.invoke(formatter, input); - String removedUnused = removeUnused.apply(formatted); - String sortedImports = (String) importOrdererMethod.invoke(null, removedUnused); - String reflowedLongStrings = reflowLongStrings.apply(sortedImports, formatter); - return fixWindowsBug(reflowedLongStrings, version); - })); + return JVM_SUPPORT.suggestLaterVersionOnError(version, googleJavaFormatFormatterFunc); } FormatterFunc createRemoveUnusedImportsOnly() throws Exception { ClassLoader classLoader = jarState.getClassLoader(); - Function removeUnused = constructRemoveUnusedFunction(classLoader); - return JVM_SUPPORT.suggestLaterVersionOnError(version, (input -> fixWindowsBug(removeUnused.apply(input), version))); - } - - private static Function constructRemoveUnusedFunction(ClassLoader classLoader) - throws NoSuchMethodException, ClassNotFoundException { - Class removeUnusedClass = classLoader.loadClass(REMOVE_UNUSED_CLASS); - Class removeJavadocOnlyClass; - try { - // google-java-format 1.7 or lower - removeJavadocOnlyClass = classLoader.loadClass(REMOVE_UNUSED_IMPORT_JavadocOnlyImports); - } catch (ClassNotFoundException e) { - // google-java-format 1.8+ - removeJavadocOnlyClass = null; - } - - Function removeUnused; - if (removeJavadocOnlyClass != null) { - @SuppressWarnings({"unchecked", "rawtypes"}) - Object removeJavadocConstant = Enum.valueOf((Class) removeJavadocOnlyClass, REMOVE_UNUSED_IMPORT_JavadocOnlyImports_Keep); - Method removeUnusedMethod = removeUnusedClass.getMethod(REMOVE_UNUSED_METHOD, String.class, removeJavadocOnlyClass); - removeUnused = (x) -> (String) removeUnusedMethod.invoke(null, x, removeJavadocConstant); - } else { - Method removeUnusedMethod = removeUnusedClass.getMethod(REMOVE_UNUSED_METHOD, String.class); - removeUnused = (x) -> (String) removeUnusedMethod.invoke(null, x); - } - return removeUnused; - } - - private static BiFunction constructReflowLongStringsFunction(ClassLoader classLoader, Class formatterClazz) throws NoSuchMethodException { - Class stringWrapperClass; - try { - stringWrapperClass = classLoader.loadClass(STRING_WRAPPER_CLASS); - } catch (ClassNotFoundException e) { - // google-java-format 1.7 or lower, which happen to be LATEST_VERSION_JRE_8, so rely on suggestJre11 for the error - return (s, f) -> { - throw e; - }; - } - Method stringWrapperMethod = stringWrapperClass.getMethod(STRING_WRAPPER_METHOD, String.class, formatterClazz); - return (s, f) -> (String) stringWrapperMethod.invoke(null, s, f); - } - } - - private static final boolean IS_WINDOWS = LineEnding.PLATFORM_NATIVE.str().equals("\r\n"); + Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.java.GoogleJavaFormatRemoveUnusedImporterFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(String.class); //version + FormatterFunc googleJavaFormatRemoveUnusedImporterFormatterFunc = (FormatterFunc) constructor.newInstance(version); - /** - * google-java-format-1.1's removeUnusedImports does *wacky* stuff on Windows. - * The beauty of normalizing all line endings to unix! - */ - static String fixWindowsBug(String input, String version) { - if (IS_WINDOWS && version.equals("1.1")) { - int firstImport = input.indexOf("\nimport "); - if (firstImport == 0) { - return input; - } else if (firstImport > 0) { - int numToTrim = 0; - char prevChar; - do { - ++numToTrim; - prevChar = input.charAt(firstImport - numToTrim); - } while (Character.isWhitespace(prevChar) && (firstImport - numToTrim) > 0); - if (firstImport - numToTrim == 0) { - // import was the very first line, and we'd like to maintain a one-line gap - ++numToTrim; - } else if (prevChar == ';' || prevChar == '/') { - // import came after either license or a package declaration - --numToTrim; - } - if (numToTrim > 0) { - return input.substring(0, firstImport - numToTrim + 2) + input.substring(firstImport + 1); - } - } + return JVM_SUPPORT.suggestLaterVersionOnError(version, googleJavaFormatRemoveUnusedImporterFormatterFunc); } - return input; } } diff --git a/lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java b/lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java index 686c9cfcaf..1e2c8a81c8 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,17 +28,26 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; + import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.SerializedFunction; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -public final class ImportOrderStep { +public final class ImportOrderStep implements Serializable { + private static final long serialVersionUID = 1L; private static final boolean WILDCARDS_LAST_DEFAULT = false; + private static final boolean SEMANTIC_SORT_DEFAULT = false; + private static final Set TREAT_AS_PACKAGE_DEFAULT = Set.of(); + private static final Set TREAT_AS_CLASS_DEFAULT = Set.of(); private final String lineFormat; @@ -55,27 +64,33 @@ private ImportOrderStep(String lineFormat) { } public FormatterStep createFrom(String... importOrder) { - return createFrom(WILDCARDS_LAST_DEFAULT, importOrder); + return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT, + TREAT_AS_CLASS_DEFAULT, importOrder); } - public FormatterStep createFrom(boolean wildcardsLast, String... importOrder) { + public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set treatAsPackage, + Set treatAsClass, String... importOrder) { // defensive copying and null checking List importOrderList = requireElementsNonNull(Arrays.asList(importOrder)); - return createFrom(wildcardsLast, () -> importOrderList); + return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> importOrderList); } public FormatterStep createFrom(File importsFile) { - return createFrom(WILDCARDS_LAST_DEFAULT, importsFile); + return createFrom(WILDCARDS_LAST_DEFAULT, SEMANTIC_SORT_DEFAULT, TREAT_AS_PACKAGE_DEFAULT, + TREAT_AS_CLASS_DEFAULT, importsFile); } - public FormatterStep createFrom(boolean wildcardsLast, File importsFile) { + public FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set treatAsPackage, + Set treatAsClass, File importsFile) { Objects.requireNonNull(importsFile); - return createFrom(wildcardsLast, () -> getImportOrder(importsFile)); + return createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, () -> getImportOrder(importsFile)); } - private FormatterStep createFrom(boolean wildcardsLast, Supplier> importOrder) { - return FormatterStep.createLazy("importOrder", - () -> new State(importOrder.get(), lineFormat, wildcardsLast), + private FormatterStep createFrom(boolean wildcardsLast, boolean semanticSort, Set treatAsPackage, + Set treatAsClass, Supplier> importOrder) { + return FormatterStep.create("importOrder", + new State(importOrder.get(), lineFormat, wildcardsLast, semanticSort, treatAsPackage, treatAsClass), + SerializedFunction.identity(), State::toFormatter); } @@ -106,15 +121,27 @@ private static final class State implements Serializable { private final List importOrder; private final String lineFormat; private final boolean wildcardsLast; - - State(List importOrder, String lineFormat, boolean wildcardsLast) { + private final boolean semanticSort; + private final TreeSet treatAsPackage; + private final TreeSet treatAsClass; + + State(List importOrder, + String lineFormat, + boolean wildcardsLast, + boolean semanticSort, + @Nullable Set treatAsPackage, + @Nullable Set treatAsClass) { this.importOrder = importOrder; this.lineFormat = lineFormat; this.wildcardsLast = wildcardsLast; + this.semanticSort = semanticSort; + this.treatAsPackage = treatAsPackage == null ? null : new TreeSet<>(treatAsPackage); + this.treatAsClass = treatAsClass == null ? null : new TreeSet<>(treatAsClass); } FormatterFunc toFormatter() { - return raw -> new ImportSorter(importOrder, wildcardsLast).format(raw, lineFormat); + return raw -> new ImportSorter(importOrder, wildcardsLast, semanticSort, treatAsPackage, treatAsClass) + .format(raw, lineFormat); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java b/lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java index edfc948487..6cd7dd4978 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ImportSorter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Scanner; +import java.util.Set; /** * @author Vojtech Krasa @@ -30,10 +31,17 @@ final class ImportSorter { private final List importsOrder; private final boolean wildcardsLast; + private final boolean semanticSort; + private final Set treatAsPackage; + private final Set treatAsClass; - ImportSorter(List importsOrder, boolean wildcardsLast) { + ImportSorter(List importsOrder, boolean wildcardsLast, boolean semanticSort, Set treatAsPackage, + Set treatAsClass) { this.importsOrder = new ArrayList<>(importsOrder); this.wildcardsLast = wildcardsLast; + this.semanticSort = semanticSort; + this.treatAsPackage = treatAsPackage; + this.treatAsClass = treatAsClass; } String format(String raw, String lineFormat) { @@ -81,7 +89,8 @@ String format(String raw, String lineFormat) { } scanner.close(); - List sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, lineFormat); + List sortedImports = ImportSorterImpl.sort(imports, importsOrder, wildcardsLast, semanticSort, + treatAsPackage, treatAsClass, lineFormat); return applyImportsToDocument(raw, firstImportLine, lastImportLine, sortedImports); } diff --git a/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java b/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java index ec075ffedd..4393de95a3 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java +++ b/lib/src/main/java/com/diffplug/spotless/java/ImportSorterImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 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.Serializable; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -25,14 +27,48 @@ // which itself is licensed under the Apache 2.0 license. final class ImportSorterImpl { - private final List template = new ArrayList<>(); + private static final String CATCH_ALL_SUBGROUP = ""; + private static final String STATIC_KEYWORD = "static "; + private static final String STATIC_SYMBOL = "\\#"; + private static final String SUBGROUP_SEPARATOR = "|"; + + private final List importsGroups; + private final Set knownGroupings = new HashSet<>(); private final Map> matchingImports = new HashMap<>(); private final List notMatching = new ArrayList<>(); private final Set allImportOrderItems = new HashSet<>(); private final Comparator ordering; - static List sort(List imports, List importsOrder, boolean wildcardsLast, String lineFormat) { - ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast); + // An ImportsGroup is a group of imports ; each group is separated by blank lines. + // A group is composed of subgroups : imports are sorted by subgroup. + private static class ImportsGroup { + + private final List subGroups; + + public ImportsGroup(String importOrder, Set knownGroupings) { + this.subGroups = Stream.of(importOrder.split("\\" + SUBGROUP_SEPARATOR, -1)) + .map(this::normalizeStatic) + .filter(group -> !knownGroupings.contains(group)) + .collect(Collectors.toList()); + knownGroupings.addAll(this.subGroups); + } + + private String normalizeStatic(String subgroup) { + if (subgroup.startsWith(STATIC_SYMBOL)) { + return subgroup.replace(STATIC_SYMBOL, STATIC_KEYWORD); + } + return subgroup; + } + + public List getSubGroups() { + return subGroups; + } + } + + static List sort(List imports, List importsOrder, boolean wildcardsLast, + boolean semanticSort, Set treatAsPackage, Set treatAsClass, String lineFormat) { + ImportSorterImpl importsSorter = new ImportSorterImpl(importsOrder, wildcardsLast, semanticSort, treatAsPackage, + treatAsClass); return importsSorter.sort(imports, lineFormat); } @@ -40,43 +76,47 @@ private List sort(List imports, String lineFormat) { filterMatchingImports(imports); mergeNotMatchingItems(false); mergeNotMatchingItems(true); - mergeMatchingItems(); + List sortedImported = mergeMatchingItems(); - return getResult(lineFormat); + return getResult(sortedImported, lineFormat); } - private ImportSorterImpl(List importOrder, boolean wildcardsLast) { - List importOrderCopy = new ArrayList<>(importOrder); - normalizeStaticOrderItems(importOrderCopy); - putStaticItemIfNotExists(importOrderCopy); - template.addAll(importOrderCopy); - ordering = new OrderingComparator(wildcardsLast); - this.allImportOrderItems.addAll(importOrderCopy); + private ImportSorterImpl(List importOrder, boolean wildcardsLast, boolean semanticSort, + Set treatAsPackage, Set treatAsClass) { + importsGroups = importOrder.stream().filter(Objects::nonNull).map(order -> new ImportsGroup(order, knownGroupings)).collect(Collectors.toList()); + putStaticItemIfNotExists(importsGroups); + putCatchAllGroupIfNotExists(importsGroups); + + if (semanticSort) { + ordering = new SemanticOrderingComparator(wildcardsLast, treatAsPackage, treatAsClass); + } else { + ordering = new LexicographicalOrderingComparator(wildcardsLast); + } + + List subgroups = importsGroups.stream().map(ImportsGroup::getSubGroups).flatMap(Collection::stream).collect(Collectors.toList()); + this.allImportOrderItems.addAll(subgroups); } - private static void putStaticItemIfNotExists(List allImportOrderItems) { - boolean contains = false; + private void putStaticItemIfNotExists(List importsGroups) { + boolean catchAllSubGroupExist = importsGroups.stream().anyMatch(group -> group.getSubGroups().contains(STATIC_KEYWORD)); + if (catchAllSubGroupExist) { + return; + } + int indexOfFirstStatic = 0; - for (int i = 0; i < allImportOrderItems.size(); i++) { - String allImportOrderItem = allImportOrderItems.get(i); - if (allImportOrderItem.equals("static ")) { - contains = true; - } - if (allImportOrderItem.startsWith("static ")) { + for (int i = 0; i < importsGroups.size(); i++) { + boolean subgroupMatch = importsGroups.get(i).getSubGroups().stream().anyMatch(subgroup -> subgroup.startsWith(STATIC_KEYWORD)); + if (subgroupMatch) { indexOfFirstStatic = i; } } - if (!contains) { - allImportOrderItems.add(indexOfFirstStatic, "static "); - } + importsGroups.add(indexOfFirstStatic, new ImportsGroup(STATIC_KEYWORD, this.knownGroupings)); } - private static void normalizeStaticOrderItems(List allImportOrderItems) { - for (int i = 0; i < allImportOrderItems.size(); i++) { - String s = allImportOrderItems.get(i); - if (s.startsWith("\\#")) { - allImportOrderItems.set(i, s.replace("\\#", "static ")); - } + private void putCatchAllGroupIfNotExists(List importsGroups) { + boolean catchAllSubGroupExist = importsGroups.stream().anyMatch(group -> group.getSubGroups().contains(CATCH_ALL_SUBGROUP)); + if (!catchAllSubGroupExist) { + importsGroups.add(new ImportsGroup(CATCH_ALL_SUBGROUP, this.knownGroupings)); } } @@ -87,9 +127,7 @@ private void filterMatchingImports(List imports) { for (String anImport : imports) { String orderItem = getBestMatchingImportOrderItem(anImport); if (orderItem != null) { - if (!matchingImports.containsKey(orderItem)) { - matchingImports.put(orderItem, new ArrayList<>()); - } + matchingImports.computeIfAbsent(orderItem, key -> new ArrayList<>()); matchingImports.get(orderItem).add(anImport); } else { notMatching.add(anImport); @@ -116,34 +154,14 @@ private void filterMatchingImports(List imports) { * not matching means it does not match any order item, so it will be appended before or after order items */ private void mergeNotMatchingItems(boolean staticItems) { - sort(notMatching); - - int firstIndexOfOrderItem = getFirstIndexOfOrderItem(notMatching, staticItems); - int indexOfOrderItem = 0; for (String notMatchingItem : notMatching) { if (!matchesStatic(staticItems, notMatchingItem)) { continue; } boolean isOrderItem = isOrderItem(notMatchingItem, staticItems); - if (isOrderItem) { - indexOfOrderItem = template.indexOf(notMatchingItem); - } else { - if (indexOfOrderItem == 0 && firstIndexOfOrderItem != 0) { - // insert before alphabetically first order item - template.add(firstIndexOfOrderItem, notMatchingItem); - firstIndexOfOrderItem++; - } else if (firstIndexOfOrderItem == 0) { - // no order is specified - if (template.size() > 0 && (template.get(template.size() - 1).startsWith("static"))) { - // insert N after last static import - template.add(ImportSorter.N); - } - template.add(notMatchingItem); - } else { - // insert after the previous order item - template.add(indexOfOrderItem + 1, notMatchingItem); - indexOfOrderItem++; - } + if (!isOrderItem) { + matchingImports.computeIfAbsent(CATCH_ALL_SUBGROUP, key -> new ArrayList<>()); + matchingImports.get(CATCH_ALL_SUBGROUP).add(notMatchingItem); } } } @@ -153,76 +171,44 @@ private boolean isOrderItem(String notMatchingItem, boolean staticItems) { return contains && matchesStatic(staticItems, notMatchingItem); } - /** - * gets first order item from sorted input list, and finds out it's index in template. - */ - private int getFirstIndexOfOrderItem(List notMatching, boolean staticItems) { - int firstIndexOfOrderItem = 0; - for (String notMatchingItem : notMatching) { - if (!matchesStatic(staticItems, notMatchingItem)) { - continue; - } - boolean isOrderItem = isOrderItem(notMatchingItem, staticItems); - if (isOrderItem) { - firstIndexOfOrderItem = template.indexOf(notMatchingItem); - break; - } - } - return firstIndexOfOrderItem; - } - private static boolean matchesStatic(boolean staticItems, String notMatchingItem) { - boolean isStatic = notMatchingItem.startsWith("static "); + boolean isStatic = notMatchingItem.startsWith(STATIC_KEYWORD); return (isStatic && staticItems) || (!isStatic && !staticItems); } - private void mergeMatchingItems() { - for (int i = 0; i < template.size(); i++) { - String item = template.get(i); - if (allImportOrderItems.contains(item)) { - // find matching items for order item - List strings = matchingImports.get(item); + private List mergeMatchingItems() { + List template = new ArrayList<>(); + for (ImportsGroup group : importsGroups) { + boolean groupIsNotEmpty = false; + for (String subgroup : group.getSubGroups()) { + List strings = matchingImports.get(subgroup); if (strings == null || strings.isEmpty()) { - // if there is none, just remove order item - template.remove(i); - i--; continue; } + groupIsNotEmpty = true; List matchingItems = new ArrayList<>(strings); sort(matchingItems); - - // replace order item by matching import statements - // this is a mess and it is only a luck that it works :-] - template.remove(i); - if (i != 0 && !template.get(i - 1).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - i++; - } - if (i + 1 < template.size() && !template.get(i + 1).equals(ImportSorter.N) - && !template.get(i).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - } - template.addAll(i, matchingItems); - if (i != 0 && !template.get(i - 1).equals(ImportSorter.N)) { - template.add(i, ImportSorter.N); - } - + template.addAll(matchingItems); + } + if (groupIsNotEmpty) { + template.add(ImportSorter.N); } } // if there is \n on the end, remove it - if (template.size() > 0 && template.get(template.size() - 1).equals(ImportSorter.N)) { + if (!template.isEmpty() && template.get(template.size() - 1).equals(ImportSorter.N)) { template.remove(template.size() - 1); } + return template; } private void sort(List items) { items.sort(ordering); } - private List getResult(String lineFormat) { + private List getResult(List sortedImported, String lineFormat) { List strings = new ArrayList<>(); - for (String s : template) { + for (String s : sortedImported) { if (s.equals(ImportSorter.N)) { strings.add(s); } else { @@ -257,30 +243,187 @@ private List getResult(String lineFormat) { return null; } - private static class OrderingComparator implements Comparator, Serializable { + private static int compareWithWildcare(String string1, String string2, boolean wildcardsLast) { + int string1WildcardIndex = string1.indexOf('*'); + int string2WildcardIndex = string2.indexOf('*'); + boolean string1IsWildcard = string1WildcardIndex >= 0; + boolean string2IsWildcard = string2WildcardIndex >= 0; + if (string1IsWildcard == string2IsWildcard) { + return string1.compareTo(string2); + } + int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex; + boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength); + if (!samePrefix) { + return string1.compareTo(string2); + } + return (string1IsWildcard == wildcardsLast) ? 1 : -1; + } + + private static class LexicographicalOrderingComparator implements Comparator, Serializable { private static final long serialVersionUID = 1; private final boolean wildcardsLast; - private OrderingComparator(boolean wildcardsLast) { + private LexicographicalOrderingComparator(boolean wildcardsLast) { this.wildcardsLast = wildcardsLast; } @Override public int compare(String string1, String string2) { - int string1WildcardIndex = string1.indexOf('*'); - int string2WildcardIndex = string2.indexOf('*'); - boolean string1IsWildcard = string1WildcardIndex >= 0; - boolean string2IsWildcard = string2WildcardIndex >= 0; - if (string1IsWildcard == string2IsWildcard) { - return string1.compareTo(string2); + return compareWithWildcare(string1, string2, wildcardsLast); + } + } + + private static class SemanticOrderingComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1; + + private final boolean wildcardsLast; + private final Set treatAsPackage; + private final Set treatAsClass; + + private SemanticOrderingComparator(boolean wildcardsLast, Set treatAsPackage, + Set treatAsClass) { + this.wildcardsLast = wildcardsLast; + this.treatAsPackage = treatAsPackage; + this.treatAsClass = treatAsClass; + } + + @Override + public int compare(String string1, String string2) { + /* + * Ordering uses semantics of the import string by splitting it into package, + * class name(s) and static member (for static imports) and then comparing by + * each of those three substrings in sequence. + * + * When comparing static imports, the last segment in the dot-separated string + * is considered to be the member (field, method, type) name. + * + * The first segment starting with an upper case letter is considered to be the + * (first) class name. Since this comparator has no actual type information, + * this auto-detection will fail for upper case package names and lower case + * class names. treatAsPackage and treatAsClass can be used respectively to + * provide hints to the auto-detection. + */ + if (string1.startsWith(STATIC_KEYWORD)) { + String[] split = splitFqcnAndMember(string1); + String fqcn1 = split[0]; + String member1 = split[1]; + + split = splitFqcnAndMember(string2); + String fqcn2 = split[0]; + String member2 = split[1]; + + int result = compareFullyQualifiedClassName(fqcn1, fqcn2); + if (result != 0) + return result; + + return compareWithWildcare(member1, member2, wildcardsLast); + } else { + return compareFullyQualifiedClassName(string1, string2); + } + } + + /** + * Compares two fully qualified class names by splitting them into package and + * (nested) class names. + */ + private int compareFullyQualifiedClassName(String fqcn1, String fqcn2) { + String[] split = splitPackageAndClasses(fqcn1); + String p1 = split[0]; + String c1 = split[1]; + + split = splitPackageAndClasses(fqcn2); + String p2 = split[0]; + String c2 = split[1]; + + int result = p1.compareTo(p2); + if (result != 0) + return result; + + return compareWithWildcare(c1, c2, wildcardsLast); + } + + /** + * Splits the provided static import string into fully qualified class name and + * the imported static member (field, method or type). + */ + private String[] splitFqcnAndMember(String importString) { + String s = importString.substring(STATIC_KEYWORD.length()).trim(); + + /* + * Static imports always contain a member or wildcard and it's always the last + * segment. + */ + int dot = s.lastIndexOf("."); + String fqcn = s.substring(0, dot); + String member = s.substring(dot + 1); + return new String[]{fqcn, member}; + } + + /** + * Splits the fully qualified class name into package and class name(s). + */ + private String[] splitPackageAndClasses(String fqcn) { + String packageNames = null; + String classNames = null; + + /* + * The first segment that starts with an upper case letter starts the class + * name(s), unless it matches treatAsPackage (then it's explicitly declared as + * package via configuration). If no segment starts with an upper case letter + * then the last segment must be a class name (unless the method input is + * garbage). + */ + int dot = fqcn.indexOf('.'); + while (dot > -1) { + int nextDot = fqcn.indexOf('.', dot + 1); + if (nextDot > -1) { + if (Character.isUpperCase(fqcn.charAt(dot + 1))) { + // if upper case, check if should be treated as package nonetheless + if (!treatAsPackage(fqcn.substring(0, nextDot))) { + packageNames = fqcn.substring(0, dot); + classNames = fqcn.substring(dot + 1); + break; + } + } else { + // if lower case, check if should be treated as class nonetheless + if (treatAsClass(fqcn.substring(0, nextDot))) { + packageNames = fqcn.substring(0, dot); + classNames = fqcn.substring(dot + 1); + break; + } + } + } + + dot = nextDot; } - int prefixLength = string1IsWildcard ? string1WildcardIndex : string2WildcardIndex; - boolean samePrefix = string1.regionMatches(0, string2, 0, prefixLength); - if (!samePrefix) { - return string1.compareTo(string2); + + if (packageNames == null) { + int i = fqcn.lastIndexOf("."); + packageNames = fqcn.substring(0, i); + classNames = fqcn.substring(i + 1); } - return (string1IsWildcard == wildcardsLast) ? 1 : -1; + + return new String[]{packageNames, classNames}; } + + /** + * Returns whether the provided prefix matches any entry of + * {@code treatAsPackage}. + */ + private boolean treatAsPackage(String prefix) { + // This would be the place to introduce wild cards or even regex matching. + return treatAsPackage != null && treatAsPackage.contains(prefix); + } + + /** + * Returns whether the provided prefix name matches any entry of + * {@code treatAsClass}. + */ + private boolean treatAsClass(String prefix) { + // This would be the place to introduce wild cards or even regex matching. + return treatAsClass != null && treatAsClass.contains(prefix); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/java/ModuleHelper.java b/lib/src/main/java/com/diffplug/spotless/java/ModuleHelper.java new file mode 100644 index 0000000000..e05a6a8b7f --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/ModuleHelper.java @@ -0,0 +1,145 @@ +/* + * Copyright 2016-2023 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.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.Jvm; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import sun.misc.Unsafe; + +final class ModuleHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleHelper.class); + + // prevent direct instantiation + private ModuleHelper() {} + + private static final Map REQUIRED_PACKAGES_TO_TEST_CLASSES = new HashMap<>(); + + static { + REQUIRED_PACKAGES_TO_TEST_CLASSES.putIfAbsent("com.sun.tools.javac.util", "Context"); + REQUIRED_PACKAGES_TO_TEST_CLASSES.putIfAbsent("com.sun.tools.javac.file", "CacheFSInfo"); + REQUIRED_PACKAGES_TO_TEST_CLASSES.putIfAbsent("com.sun.tools.javac.tree", "TreeTranslator"); + REQUIRED_PACKAGES_TO_TEST_CLASSES.putIfAbsent("com.sun.tools.javac.parser", "Tokens$TokenKind"); + REQUIRED_PACKAGES_TO_TEST_CLASSES.putIfAbsent("com.sun.tools.javac.api", "DiagnosticFormatter$PositionKind"); + } + + private static boolean checkDone = false; + + public static synchronized void doOpenInternalPackagesIfRequired() { + if (Jvm.version() < 16 || checkDone) { + return; + } + try { + checkDone = true; + final List unavailableRequiredPackages = unavailableRequiredPackages(); + if (!unavailableRequiredPackages.isEmpty()) { + openPackages(unavailableRequiredPackages); + final List failedToOpen = unavailableRequiredPackages(); + if (!failedToOpen.isEmpty()) { + final StringBuilder message = new StringBuilder(); + message.append("WARNING: Some required internal classes are unavailable. Please consider adding the following JVM arguments\n"); + message.append("WARNING: "); + for (String name : failedToOpen) { + message.append(String.format("--add-opens jdk.compiler/%s=ALL-UNNAMED", name)); + } + LOGGER.warn("{}", message); + } + } + } catch (Throwable e) { + LOGGER.error("WARNING: Failed to check for available JDK packages.", e); + } + } + + @SuppressFBWarnings("REC_CATCH_EXCEPTION") // workaround JDK11 + private static List unavailableRequiredPackages() { + final List packages = new ArrayList<>(); + for (Map.Entry e : REQUIRED_PACKAGES_TO_TEST_CLASSES.entrySet()) { + final String key = e.getKey(); + final String value = e.getValue(); + try { + final Class clazz = Class.forName(key + "." + value); + if (clazz.isEnum()) { + clazz.getMethod("values").invoke(null); + } else { + clazz.getDeclaredConstructor().newInstance(); + } + } catch (IllegalAccessException ex) { + packages.add(key); + } catch (Exception ignore) { + // in old versions of JDK some classes could be unavailable + } + } + return packages; + } + + @SuppressWarnings("unchecked") + private static void openPackages(Collection packagesToOpen) throws Throwable { + final Collection modules = allModules(); + if (modules == null) { + return; + } + final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + final Unsafe unsafe = (Unsafe) unsafeField.get(null); + final Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + final MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafe.getObject( + unsafe.staticFieldBase(implLookupField), + unsafe.staticFieldOffset(implLookupField)); + final MethodHandle modifiers = lookup.findSetter(Method.class, "modifiers", Integer.TYPE); + final Method exportMethod = Class.forName("java.lang.Module").getDeclaredMethod("implAddOpens", String.class); + modifiers.invokeExact(exportMethod, Modifier.PUBLIC); + for (Object module : modules) { + final Collection packages = (Collection) module.getClass().getMethod("getPackages").invoke(module); + for (String name : packages) { + if (packagesToOpen.contains(name)) { + exportMethod.invoke(module, name); + } + } + } + } + + @Nullable + @SuppressFBWarnings("REC_CATCH_EXCEPTION") // workaround JDK11 + private static Collection allModules() { + // calling ModuleLayer.boot().modules() by reflection + try { + final Object boot = Class.forName("java.lang.ModuleLayer").getMethod("boot").invoke(null); + if (boot == null) { + return null; + } + final Object modules = boot.getClass().getMethod("modules").invoke(boot); + return (Collection) modules; + } catch (Exception ignore) { + return null; + } + } +} 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 a5474d549b..7745f6e879 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-2022 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,31 @@ /** Wraps up palantir-java-format fork of * google-java-format as a FormatterStep. */ -public class PalantirJavaFormatStep { - // prevent direct instantiation - private PalantirJavaFormatStep() {} - +public class PalantirJavaFormatStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final boolean DEFAULT_FORMAT_JAVADOC = false; + private static final String DEFAULT_STYLE = "PALANTIR"; private static final String NAME = "palantir-java-format"; - private static final String MAVEN_COORDINATE = "com.palantir.javaformat:palantir-java-format:"; - private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "1.1.0").add(11, "2.10.0"); + public static final String MAVEN_COORDINATE = "com.palantir.javaformat:palantir-java-format:"; + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(8, "1.1.0").add(11, "2.28.0").add(21, "2.57.0"); + + /** The jar that contains the formatter. */ + private final JarState.Promised jarState; + /** Version of the formatter jar. */ + private final String formatterVersion; + private final String style; + /** Whether to format Java docs. */ + private final boolean formatJavadoc; + + private PalantirJavaFormatStep(JarState.Promised jarState, + String formatterVersion, + String style, + boolean formatJavadoc) { + this.jarState = jarState; + this.formatterVersion = formatterVersion; + this.style = style; + this.formatJavadoc = formatJavadoc; + } /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(Provisioner provisioner) { @@ -38,11 +56,29 @@ public static FormatterStep create(Provisioner provisioner) { /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(String version, Provisioner provisioner) { + return create(version, defaultStyle(), provisioner); + } + + /** + * Creates a step which formats code, import order, and unused imports, but not Java docs. And with the given format + * style. + */ + public static FormatterStep create(String version, String style, Provisioner provisioner) { + return create(version, style, DEFAULT_FORMAT_JAVADOC, provisioner); + } + + /** + * Creates a step which formats everything - code, import order, unused imports, and Java docs. And with the given + * format style. + */ + public static FormatterStep create(String version, String style, boolean formatJavadoc, Provisioner provisioner) { Objects.requireNonNull(version, "version"); + Objects.requireNonNull(style, "style"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(JarState.from(MAVEN_COORDINATE + version, provisioner), version), + return FormatterStep.create(NAME, + new PalantirJavaFormatStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), version, style, formatJavadoc), + PalantirJavaFormatStep::equalityState, State::createFormat); } @@ -51,24 +87,42 @@ public static String defaultVersion() { return JVM_SUPPORT.getRecommendedFormatterVersion(); } + /** Get default style */ + public static String defaultStyle() { + return DEFAULT_STYLE; + } + + /** Get default for whether Java docs should be formatted */ + public static boolean defaultFormatJavadoc() { + return DEFAULT_FORMAT_JAVADOC; + } + + private State equalityState() { + return new State(jarState.get(), formatterVersion, style, formatJavadoc); + } + private static final class State implements Serializable { private static final long serialVersionUID = 1L; - /** The jar that contains the formatter. */ private final JarState jarState; - /** Version of the formatter jar. */ private final String formatterVersion; + private final String style; + private final boolean formatJavadoc; - State(JarState jarState, String formatterVersion) { + State(JarState jarState, String formatterVersion, String style, boolean formatJavadoc) { + ModuleHelper.doOpenInternalPackagesIfRequired(); this.jarState = jarState; this.formatterVersion = formatterVersion; + this.style = style; + this.formatJavadoc = formatJavadoc; } FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); final Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.pjf.PalantirJavaFormatFormatterFunc"); - final Constructor constructor = formatterFunc.getConstructor(); - return JVM_SUPPORT.suggestLaterVersionOnError(formatterVersion, (FormatterFunc) constructor.newInstance()); + // 1st arg is "style", 2nd arg is "formatJavadoc" + final Constructor constructor = formatterFunc.getConstructor(String.class, boolean.class); + return JVM_SUPPORT.suggestLaterVersionOnError(formatterVersion, (FormatterFunc) constructor.newInstance(style, formatJavadoc)); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java b/lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java index 91ce3cc023..753678a86b 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,22 +15,45 @@ */ package com.diffplug.spotless.java; +import java.io.Serializable; +import java.util.List; import java.util.Objects; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.Provisioner; -/** Uses google-java-format, but only to remove unused imports. */ -public class RemoveUnusedImportsStep { +/** Uses google-java-format or cleanthat.UnnecessaryImport, but only to remove unused imports. */ +public class RemoveUnusedImportsStep implements Serializable { + private static final long serialVersionUID = 1L; + static final String NAME = "removeUnusedImports"; + + static final String GJF = "google-java-format"; + static final String CLEANTHAT = "cleanthat-javaparser-unnecessaryimport"; + + // https://github.com/solven-eu/cleanthat/blob/master/java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/UnnecessaryImport.java + private static final String CLEANTHAT_MUTATOR = "UnnecessaryImport"; + // prevent direct instantiation private RemoveUnusedImportsStep() {} - static final String NAME = "removeUnusedImports"; + public static String defaultFormatter() { + return GJF; + } public static FormatterStep create(Provisioner provisioner) { + // The default importRemover is GJF + return create(GJF, provisioner); + } + + public static FormatterStep create(String unusedImportRemover, Provisioner provisioner) { Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new GoogleJavaFormatStep.State(NAME, GoogleJavaFormatStep.defaultVersion(), provisioner), - GoogleJavaFormatStep.State::createRemoveUnusedImportsOnly); + switch (unusedImportRemover) { + case GJF: + return GoogleJavaFormatStep.createRemoveUnusedImportsOnly(provisioner); + case CLEANTHAT: + return CleanthatJavaStep.createWithStepName(NAME, CleanthatJavaStep.defaultGroupArtifact(), CleanthatJavaStep.defaultVersion(), "99.9", List.of(CLEANTHAT_MUTATOR), List.of(), false, provisioner); + default: + throw new IllegalArgumentException("Invalid unusedImportRemover: " + unusedImportRemover); + } } } diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java new file mode 100644 index 0000000000..6a35a1e693 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023-2024 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.json; + +import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * A DTO holding the basic for Jackson-based formatters + */ +public class JacksonConfig implements Serializable { + private static final long serialVersionUID = 1L; + + private static final Map DEFAULT_FEATURE_TOGGLES; + + static { + Map defaultFeatureToggles = new LinkedHashMap<>(); + // We activate by default the PrettyPrinter from Jackson + // @see com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT + defaultFeatureToggles.put("INDENT_OUTPUT", true); + DEFAULT_FEATURE_TOGGLES = defaultFeatureToggles; + } + + protected Map featureToToggle = new TreeMap<>(DEFAULT_FEATURE_TOGGLES); + + public Map getFeatureToToggle() { + return Collections.unmodifiableMap(featureToToggle); + } + + public void setFeatureToToggle(Map featureToToggle) { + this.featureToToggle = featureToToggle; + } + + public void appendFeatureToToggle(Map features) { + this.featureToToggle.putAll(features); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java new file mode 100644 index 0000000000..efff594663 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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.json; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Specialization of {@link JacksonConfig} for JSON documents + */ +public class JacksonJsonConfig extends JacksonConfig { + private static final long serialVersionUID = 1L; + + protected Map jsonFeatureToToggle = new LinkedHashMap<>(); + + // https://github.com/revelc/formatter-maven-plugin/pull/280 + // By default, Jackson adds a ' ' before separator, which is not standard with most IDE/JSON libraries + protected boolean spaceBeforeSeparator = false; + + public Map getJsonFeatureToToggle() { + return Collections.unmodifiableMap(jsonFeatureToToggle); + } + + /** + * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature + */ + public void setJsonFeatureToToggle(Map jsonFeatureToToggle) { + this.jsonFeatureToToggle = jsonFeatureToToggle; + } + + /** + * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature + */ + public void appendJsonFeatureToToggle(Map features) { + this.jsonFeatureToToggle.putAll(features); + } + + public boolean isSpaceBeforeSeparator() { + return spaceBeforeSeparator; + } + + public void setSpaceBeforeSeparator(boolean spaceBeforeSeparator) { + this.spaceBeforeSeparator = spaceBeforeSeparator; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java new file mode 100644 index 0000000000..a4d264b4f0 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021-2024 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.json; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +/** + * Simple YAML formatter which reformats the file according to Jackson YAMLFactory. + */ +// https://stackoverflow.com/questions/14515994/convert-json-string-to-pretty-print-json-output-using-jackson +public class JacksonJsonStep implements Serializable { + 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.18.1"; + public static final String NAME = "jacksonJson"; + + private final JarState.Promised jarState; + private final JacksonConfig jacksonConfig; + + private JacksonJsonStep(JarState.Promised jarState, JacksonConfig jacksonConfig) { + this.jarState = jarState; + this.jacksonConfig = jacksonConfig; + } + + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + public static FormatterStep create(Provisioner provisioner) { + return create(new JacksonJsonConfig(), defaultVersion(), provisioner); + } + + public static FormatterStep create(JacksonJsonConfig jacksonConfig, + String jacksonVersion, + Provisioner provisioner) { + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new JacksonJsonStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + jacksonVersion, provisioner)), jacksonConfig), + JacksonJsonStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get(), jacksonConfig); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final JacksonConfig jacksonConfig; + private final JarState jarState; + + State(JarState jarState, JacksonConfig jacksonConfig) { + this.jarState = jarState; + this.jacksonConfig = jacksonConfig; + } + + FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + InstantiationException, IllegalAccessException { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.json.JacksonJsonFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(JacksonJsonConfig.class); + return (FormatterFunc) constructor.newInstance(jacksonConfig); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java b/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java new file mode 100644 index 0000000000..2589e986a2 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023-2024 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.json; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Map; +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.Provisioner; + +public class JsonPatchStep implements Serializable { + 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; + @Nullable + private final List> patch; + @Nullable + private final String patchString; + + private JsonPatchStep(JarState.Promised jarState, + @Nullable String patchString, + @Nullable List> patch) { + this.jarState = jarState; + this.patchString = patchString; + this.patch = patch; + } + + public static FormatterStep create(String patchString, Provisioner provisioner) { + return create(DEFAULT_VERSION, patchString, provisioner); + } + + public static FormatterStep create(String zjsonPatchVersion, String patchString, Provisioner provisioner) { + Objects.requireNonNull(zjsonPatchVersion, "zjsonPatchVersion cannot be null"); + Objects.requireNonNull(patchString, "patchString cannot be null"); + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new JsonPatchStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + ":" + zjsonPatchVersion, provisioner)), patchString, null), + JsonPatchStep::equalityState, + State::toFormatter); + } + + public static FormatterStep create(List> patch, Provisioner provisioner) { + return create(DEFAULT_VERSION, patch, provisioner); + } + + public static FormatterStep create(String zjsonPatchVersion, List> patch, Provisioner provisioner) { + Objects.requireNonNull(zjsonPatchVersion, "zjsonPatchVersion cannot be null"); + Objects.requireNonNull(patch, "patch cannot be null"); + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new JsonPatchStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + ":" + zjsonPatchVersion, provisioner)), null, patch), + JsonPatchStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get(), patchString, patch); + } + + static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final JarState jarState; + @Nullable + private final List> patch; + @Nullable + private final String patchString; + + State(JarState jarState, + @Nullable String patchString, + @Nullable List> patch) { + this.jarState = jarState; + this.patchString = patchString; + this.patch = patch; + } + + FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.json.JsonPatchFormatterFunc"); + if (this.patch != null) { + Constructor constructor = formatterFunc.getConstructor(List.class); + return (FormatterFunc) constructor.newInstance(patch); + } else { + Constructor constructor = formatterFunc.getConstructor(String.class); + return (FormatterFunc) constructor.newInstance(patchString); + } + } + } +} 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 a970149133..c42bf3cf9c 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 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package com.diffplug.spotless.json; -import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -30,13 +29,30 @@ /** * Simple JSON formatter which reformats the file according to the org.json library's default pretty-printing, but has no ability to customise more than the indentation size. */ -public final class JsonSimpleStep { +public final class JsonSimpleStep implements Serializable { + 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; + private final int indentSpaces; + + private JsonSimpleStep(JarState.Promised jarState, int indentSpaces) { + this.indentSpaces = indentSpaces; + this.jarState = jarState; + } public static FormatterStep create(int indent, Provisioner provisioner) { Objects.requireNonNull(provisioner, "provisioner cannot be null"); - return FormatterStep.createLazy("json", () -> new State(indent, provisioner), State::toFormatter); + return FormatterStep.create(NAME, + new JsonSimpleStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + DEFAULT_VERSION, provisioner)), indent), + JsonSimpleStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get(), indentSpaces); } private static final class State implements Serializable { @@ -45,9 +61,9 @@ private static final class State implements Serializable { private final int indentSpaces; private final JarState jarState; - private State(int indent, Provisioner provisioner) throws IOException { + State(JarState jarState, int indent) { + this.jarState = jarState; this.indentSpaces = indent; - this.jarState = JarState.from(MAVEN_COORDINATE + DEFAULT_VERSION, provisioner); } FormatterFunc toFormatter() { @@ -81,7 +97,7 @@ FormatterFunc toFormatter() { return format(arrayConstructor, arrayToString, s); } - throw new AssertionError(String.format("Unable to determine JSON type, expected a '{' or '[' but found '%s'", first)); + throw new IllegalArgumentException(String.format("Unable to determine JSON type, expected a '{' or '[' but found '%s'", first)); }; } @@ -89,13 +105,9 @@ private String format(Constructor constructor, Method toString, String input) try { Object parsed = constructor.newInstance(input); return toString.invoke(parsed, indentSpaces) + "\n"; - } catch (InvocationTargetException ex) { - throw new AssertionError("Unable to format JSON", ex.getCause()); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException("Unable to format JSON", e); } } } - - private JsonSimpleStep() { - // cannot be directly instantiated - } } diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java new file mode 100644 index 0000000000..a11c1ee296 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.json.gson; + +import java.io.Serializable; + +public class GsonConfig implements Serializable { + private static final long serialVersionUID = 6039715618937332633L; + + private boolean sortByKeys; + private boolean escapeHtml; + private int indentSpaces; + private String version; + + public GsonConfig(boolean sortByKeys, boolean escapeHtml, int indentSpaces, String version) { + this.sortByKeys = sortByKeys; + this.escapeHtml = escapeHtml; + this.indentSpaces = indentSpaces; + this.version = version; + } + + public boolean isSortByKeys() { + return sortByKeys; + } + + public void setSortByKeys(boolean sortByKeys) { + this.sortByKeys = sortByKeys; + } + + public boolean isEscapeHtml() { + return escapeHtml; + } + + public void setEscapeHtml(boolean escapeHtml) { + this.escapeHtml = escapeHtml; + } + + public int getIndentSpaces() { + return indentSpaces; + } + + public void setIndentSpaces(int indentSpaces) { + this.indentSpaces = indentSpaces; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} 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 new file mode 100644 index 0000000000..a690b02b95 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022-2024 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.json.gson; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class GsonStep implements Serializable { + private static final long serialVersionUID = 1L; + 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.11.0"; + + private final JarState.Promised jarState; + private final GsonConfig gsonConfig; + + private GsonStep(JarState.Promised jarState, GsonConfig gsonConfig) { + this.gsonConfig = gsonConfig; + this.jarState = jarState; + } + + public static FormatterStep create(GsonConfig gsonConfig, Provisioner provisioner) { + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new GsonStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATES + ":" + gsonConfig.getVersion(), provisioner)), gsonConfig), + GsonStep::equalityState, + State::toFormatter); + } + + private State equalityState() { + return new State(jarState.get(), gsonConfig); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = -3240568265160440420L; + + private final JarState jarState; + private final GsonConfig gsonConfig; + + private State(JarState jarState, GsonConfig gsonConfig) { + this.jarState = jarState; + this.gsonConfig = gsonConfig; + } + + FormatterFunc toFormatter() { + try { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.gson.GsonFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(GsonConfig.class); + return (FormatterFunc) constructor.newInstance(gsonConfig); + } 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/json/package-info.java b/lib/src/main/java/com/diffplug/spotless/json/package-info.java index 0d62356d77..625de57180 100644 --- a/lib/src/main/java/com/diffplug/spotless/json/package-info.java +++ b/lib/src/main/java/com/diffplug/spotless/json/package-info.java @@ -1,6 +1,6 @@ @ParametersAreNonnullByDefault @ReturnValuesAreNonnullByDefault -package com.diffplug.spotless.extra.json.java; +package com.diffplug.spotless.json; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/BadSemver.java b/lib/src/main/java/com/diffplug/spotless/kotlin/BadSemver.java index ae500e55b4..771c27eb3e 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/BadSemver.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/BadSemver.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,18 @@ protected static int version(String input) { } String major = matcher.group(1); String minor = matcher.group(2); - return version(Integer.parseInt(major), Integer.parseInt(minor)); + String patch = matcher.group(3); + return version(Integer.parseInt(major), Integer.parseInt(minor), patch != null ? Integer.parseInt(patch) : 0); } /** Ambiguous after 2147.483647.blah-blah */ + protected static int version(int major, int minor, int patch) { + return major * 1_000_000 + minor * 1_000 + patch; + } + protected static int version(int major, int minor) { - return major * 1_000_000 + minor; + return version(major, minor, 0); } - private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)"); + private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)\\.*(\\d+)*"); } 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 7f5779cce6..daf101d9ce 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-2022 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,9 @@ */ package com.diffplug.spotless.kotlin; -import java.io.IOException; +import java.io.File; import java.io.Serializable; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.*; import javax.annotation.Nullable; @@ -28,16 +25,28 @@ import com.diffplug.spotless.*; /** Wraps up diktat as a FormatterStep. */ -public class DiktatStep { +public class DiktatStep implements Serializable { + private static final long serialVersionUID = 1L; + private final JarState.Promised jarState; + private final String versionDiktat; + private final boolean isScript; + private final @Nullable FileSignature.Promised config; + + private DiktatStep(JarState.Promised jarState, String versionDiktat, boolean isScript, @Nullable FileSignature config) { + this.jarState = jarState; + this.versionDiktat = versionDiktat; + this.isScript = isScript; + this.config = config != null ? config.asPromise() : null; + } + + private static final String MIN_SUPPORTED_VERSION = "1.2.1"; - // prevent direct instantiation - private DiktatStep() {} + private static final String PACKAGE_RELOCATED_VERSION = "2.0.0"; - private static final String DEFAULT_VERSION = "1.0.1"; - static final String NAME = "diktat"; - static final String PACKAGE_DIKTAT = "org.cqfn.diktat"; - static final String PACKAGE_KTLINT = "com.pinterest.ktlint"; - static final String MAVEN_COORDINATE = PACKAGE_DIKTAT + ":diktat-rules:"; + 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; @@ -48,141 +57,58 @@ public static FormatterStep create(Provisioner provisioner) { } public static FormatterStep create(String versionDiktat, Provisioner provisioner) { - return create(versionDiktat, provisioner, Collections.emptyMap(), null); + return create(versionDiktat, provisioner, null); } public static FormatterStep create(String versionDiktat, Provisioner provisioner, @Nullable FileSignature config) { - return create(versionDiktat, provisioner, Collections.emptyMap(), config); - } - - public static FormatterStep create(String versionDiktat, Provisioner provisioner, Map userData, @Nullable FileSignature config) { - return create(versionDiktat, provisioner, false, userData, config); - } - - public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, @Nullable FileSignature config) { - return createForScript(versionDiktat, provisioner, Collections.emptyMap(), config); - } - - public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, Map userData, @Nullable FileSignature config) { - return create(versionDiktat, provisioner, true, userData, config); + return create(versionDiktat, provisioner, false, config); } - public static FormatterStep create(String versionDiktat, Provisioner provisioner, boolean isScript, Map userData, @Nullable FileSignature config) { + public static FormatterStep create(String versionDiktat, Provisioner provisioner, boolean isScript, @Nullable FileSignature config) { + if (BadSemver.version(versionDiktat) < BadSemver.version(MIN_SUPPORTED_VERSION)) { + throw new IllegalStateException("Minimum required Diktat version is " + MIN_SUPPORTED_VERSION + ", you tried " + versionDiktat + " which is too old"); + } Objects.requireNonNull(versionDiktat, "versionDiktat"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new DiktatStep.State(versionDiktat, provisioner, isScript, userData, config), - DiktatStep.State::createFormat); + final String diktatCoordinate; + if (BadSemver.version(versionDiktat) >= BadSemver.version(PACKAGE_RELOCATED_VERSION)) { + diktatCoordinate = MAVEN_COORDINATE + versionDiktat; + } else { + diktatCoordinate = MAVEN_COORDINATE_PRE_2_0_0 + versionDiktat; + } + return FormatterStep.create(NAME, + new DiktatStep(JarState.promise(() -> JarState.from(diktatCoordinate, provisioner)), versionDiktat, isScript, config), + DiktatStep::equalityState, State::createFormat); } - static final class State implements Serializable { + private State equalityState() throws Exception { + return new State(jarState.get(), versionDiktat, isScript, config != null ? config.get() : null); + } + static final class State implements Serializable { private static final long serialVersionUID = 1L; + final JarState jar; + final String versionDiktat; /** Are the files being linted Kotlin script files. */ private final boolean isScript; private final @Nullable FileSignature config; - private final String pkg; - private final String pkgKtlint; - final JarState jar; - private final TreeMap userData; - State(String versionDiktat, Provisioner provisioner, boolean isScript, Map userData, @Nullable FileSignature config) throws IOException { - - HashSet pkgSet = new HashSet<>(); - pkgSet.add(MAVEN_COORDINATE + versionDiktat); - - this.userData = new TreeMap<>(userData); - this.pkg = PACKAGE_DIKTAT; - this.pkgKtlint = PACKAGE_KTLINT; - this.jar = JarState.from(pkgSet, provisioner); + State(JarState jar, String versionDiktat, boolean isScript, @Nullable FileSignature config) { + this.jar = jar; + this.versionDiktat = versionDiktat; this.isScript = isScript; this.config = config; } FormatterFunc createFormat() throws Exception { - - ClassLoader classLoader = jar.getClassLoader(); - - // first, we get the diktat rules - if (config != null) { - System.setProperty("diktat.config.path", config.getOnlyFile().getAbsolutePath()); - } - - Class ruleSetProviderClass = classLoader.loadClass(pkg + ".ruleset.rules.DiktatRuleSetProvider"); - Object diktatRuleSet = ruleSetProviderClass.getMethod("get").invoke(ruleSetProviderClass.newInstance()); - Iterable ruleSets = Collections.singletonList(diktatRuleSet); - - // next, we create an error callback which throws an assertion error when the format is bad - Class function2Interface = classLoader.loadClass("kotlin.jvm.functions.Function2"); - Class lintErrorClass = classLoader.loadClass(pkgKtlint + ".core.LintError"); - Method detailGetter = lintErrorClass.getMethod("getDetail"); - Method lineGetter = lintErrorClass.getMethod("getLine"); - Method colGetter = lintErrorClass.getMethod("getCol"); - - // grab the KtLint singleton - Class ktlintClass = classLoader.loadClass(pkgKtlint + ".core.KtLint"); - Object ktlint = ktlintClass.getDeclaredField("INSTANCE").get(null); - - Class paramsClass = classLoader.loadClass(pkgKtlint + ".core.KtLint$Params"); - // and its constructor - Constructor constructor = paramsClass.getConstructor( - /* fileName, nullable */ String.class, - /* text */ String.class, - /* ruleSets */ Iterable.class, - /* userData */ Map.class, - /* callback */ function2Interface, - /* script */ boolean.class, - /* editorConfigPath, nullable */ String.class, - /* debug */ boolean.class); - Method formatterMethod = ktlintClass.getMethod("format", paramsClass); - FormatterFunc.NeedsFile formatterFunc = (input, file) -> { - ArrayList errors = new ArrayList<>(); - - Object formatterCallback = Proxy.newProxyInstance(classLoader, new Class[]{function2Interface}, - (proxy, method, args) -> { - Object lintError = args[0]; //ktlint.core.LintError - boolean corrected = (Boolean) args[1]; - if (!corrected) { - errors.add(lintError); - } - return null; - }); - - userData.put("file_path", file.getAbsolutePath()); - try { - Object params = constructor.newInstance( - /* fileName, nullable */ file.getName(), - /* text */ input, - /* ruleSets */ ruleSets, - /* userData */ userData, - /* callback */ formatterCallback, - /* script */ isScript, - /* editorConfigPath, nullable */ null, - /* debug */ false); - String result = (String) formatterMethod.invoke(ktlint, params); - if (!errors.isEmpty()) { - StringBuilder error = new StringBuilder(""); - error.append("There are ").append(errors.size()).append(" unfixed errors:"); - for (Object er : errors) { - String detail = (String) detailGetter.invoke(er); - int line = (Integer) lineGetter.invoke(er); - int col = (Integer) colGetter.invoke(er); - - error.append(System.lineSeparator()).append("Error on line: ").append(line).append(", column: ").append(col).append(" cannot be fixed automatically") - .append(System.lineSeparator()).append(detail); - } - throw new AssertionError(error); - } - return result; - } catch (InvocationTargetException e) { - throw ThrowingEx.unwrapCause(e); - } - }; - - return formatterFunc; + final File configFile = (config != null) ? config.getOnlyFile() : null; + Class formatterFunc = jar.getClassLoader().loadClass("com.diffplug.spotless.glue.diktat.DiktatFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor( + String.class, + File.class, + boolean.class); + return (FormatterFunc.NeedsFile) constructor.newInstance(versionDiktat, configFile, isScript); } - } - } diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/KotlinConstants.java b/lib/src/main/java/com/diffplug/spotless/kotlin/KotlinConstants.java index 5df0231b8d..da2240f16c 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KotlinConstants.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KotlinConstants.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2022 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,7 @@ public final class KotlinConstants { // '^' is prepended to the regex in LICENSE_HEADER_DELIMITER later in FormatExtension.licenseHeader(String, String) - public static final String LICENSE_HEADER_DELIMITER = "(package |@file)"; + public static final String LICENSE_HEADER_DELIMITER = "(package |@file|import )"; private KotlinConstants() {} } 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 1db75a4e1f..2d0929f60e 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,60 +15,69 @@ */ package com.diffplug.spotless.kotlin; -import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.TreeMap; +import javax.annotation.Nullable; + +import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; -import com.diffplug.spotless.ThrowingEx; /** Wraps up ktlint as a FormatterStep. */ -public class KtLintStep { - // prevent direct instantiation - private KtLintStep() {} - - private static final String DEFAULT_VERSION = "0.43.2"; - static final String NAME = "ktlint"; - static final String PACKAGE_PRE_0_32 = "com.github.shyiko"; - static final String PACKAGE = "com.pinterest"; - static final String MAVEN_COORDINATE_PRE_0_32 = PACKAGE_PRE_0_32 + ":ktlint:"; - static final String MAVEN_COORDINATE = PACKAGE + ":ktlint:"; +public class KtLintStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_VERSION = "1.5.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:"; + + private final JarState.Promised jarState; + @Nullable + private final FileSignature.Promised config; + private final Map editorConfigOverride; + private final String version; + + private KtLintStep(String version, + JarState.Promised jarState, + @Nullable FileSignature config, + Map editorConfigOverride) { + this.version = version; + this.jarState = jarState; + this.config = config != null ? config.asPromise() : null; + this.editorConfigOverride = editorConfigOverride; + } public static FormatterStep create(Provisioner provisioner) { return create(defaultVersion(), provisioner); } public static FormatterStep create(String version, Provisioner provisioner) { - return create(version, provisioner, Collections.emptyMap()); - } - - public static FormatterStep create(String version, Provisioner provisioner, Map userData) { - return create(version, provisioner, false, userData); - } - - public static FormatterStep createForScript(String version, Provisioner provisioner) { - return create(version, provisioner, true, Collections.emptyMap()); - } - - public static FormatterStep createForScript(String version, Provisioner provisioner, Map userData) { - return create(version, provisioner, true, userData); + return create(version, provisioner, null, Collections.emptyMap(), Collections.emptyList()); } - private static FormatterStep create(String version, Provisioner provisioner, boolean isScript, Map userData) { + public static FormatterStep create(String version, + Provisioner provisioner, + @Nullable FileSignature editorConfig, + Map editorConfigOverride, + List customRuleSets) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(version, provisioner, isScript, userData), + String ktlintCoordinate = (version.startsWith("0.") ? MAVEN_COORDINATE_0_DOT : MAVEN_COORDINATE_1_DOT) + version; + Set mavenCoordinates = new HashSet<>(customRuleSets); + mavenCoordinates.add(ktlintCoordinate); + return FormatterStep.create(NAME, + new KtLintStep(version, JarState.promise(() -> JarState.from(mavenCoordinates, provisioner)), editorConfig, editorConfigOverride), + KtLintStep::equalityState, State::createFormat); } @@ -76,80 +85,35 @@ public static String defaultVersion() { return DEFAULT_VERSION; } - static final class State implements Serializable { - private static final long serialVersionUID = 1L; + private State equalityState() { + return new State(version, jarState.get(), config != null ? config.get() : null, editorConfigOverride); + } - /** Are the files being linted Kotlin script files. */ - private final boolean isScript; - private final String pkg; + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; /** The jar that contains the formatter. */ - final JarState jarState; - private final TreeMap userData; - private final boolean useParams; - - State(String version, Provisioner provisioner, boolean isScript, Map userData) throws IOException { - this.userData = new TreeMap<>(userData); - String coordinate; - if (BadSemver.version(version) < BadSemver.version(0, 32)) { - coordinate = MAVEN_COORDINATE_PRE_0_32; - this.pkg = PACKAGE_PRE_0_32; - } else { - coordinate = MAVEN_COORDINATE; - this.pkg = PACKAGE; - } - this.useParams = BadSemver.version(version) >= BadSemver.version(0, 34); - this.jarState = JarState.from(coordinate + version, provisioner); - this.isScript = isScript; + private final JarState jarState; + private final TreeMap editorConfigOverride; + private final String version; + @Nullable + private final FileSignature editorConfigPath; + + State(String version, + JarState jarState, + @Nullable FileSignature editorConfigPath, + Map editorConfigOverride) { + this.version = version; + this.jarState = jarState; + this.editorConfigOverride = new TreeMap<>(editorConfigOverride); + this.editorConfigPath = editorConfigPath; } FormatterFunc createFormat() throws Exception { - if (useParams) { - Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.ktlint.KtlintFormatterFunc"); - Constructor constructor = formatterFunc.getConstructor(boolean.class, Map.class); - return (FormatterFunc.NeedsFile) constructor.newInstance(isScript, userData); - } - - ClassLoader classLoader = jarState.getClassLoader(); - // String KtLint::format(String input, Iterable rules, Function2 errorCallback) - - // first, we get the standard rules - Class standardRuleSetProviderClass = classLoader.loadClass(pkg + ".ktlint.ruleset.standard.StandardRuleSetProvider"); - Object standardRuleSet = standardRuleSetProviderClass.getMethod("get").invoke(standardRuleSetProviderClass.newInstance()); - Iterable ruleSets = Collections.singletonList(standardRuleSet); - - // next, we create an error callback which throws an assertion error when the format is bad - Class function2Interface = classLoader.loadClass("kotlin.jvm.functions.Function2"); - Class lintErrorClass = classLoader.loadClass(pkg + ".ktlint.core.LintError"); - Method detailGetter = lintErrorClass.getMethod("getDetail"); - Method lineGetter = lintErrorClass.getMethod("getLine"); - Method colGetter = lintErrorClass.getMethod("getCol"); - Object formatterCallback = Proxy.newProxyInstance(classLoader, new Class[]{function2Interface}, - (proxy, method, args) -> { - Object lintError = args[0]; //ktlint.core.LintError - boolean corrected = (Boolean) args[1]; - if (!corrected) { - String detail = (String) detailGetter.invoke(lintError); - int line = (Integer) lineGetter.invoke(lintError); - int col = (Integer) colGetter.invoke(lintError); - throw new AssertionError("Error on line: " + line + ", column: " + col + "\n" + detail); - } - return null; - }); - - // grab the KtLint singleton - Class ktlintClass = classLoader.loadClass(pkg + ".ktlint.core.KtLint"); - Object ktlint = ktlintClass.getDeclaredField("INSTANCE").get(null); - - // and its format method - String formatterMethodName = isScript ? "formatScript" : "format"; - Method formatterMethod = ktlintClass.getMethod(formatterMethodName, String.class, Iterable.class, Map.class, function2Interface); - return input -> { - try { - return (String) formatterMethod.invoke(ktlint, input, ruleSets, userData, formatterCallback); - } catch (InvocationTargetException e) { - throw ThrowingEx.unwrapCause(e); - } - }; + final ClassLoader classLoader = jarState.getClassLoader(); + Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.ktlint.KtlintFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor( + String.class, FileSignature.class, Map.class); + return (FormatterFunc.NeedsFile) constructor.newInstance(version, editorConfigPath, editorConfigOverride); } } } 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 b3ad0041f6..2120dc144a 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,83 @@ package com.diffplug.spotless.kotlin; import static com.diffplug.spotless.kotlin.KtfmtStep.Style.DEFAULT; +import static com.diffplug.spotless.kotlin.KtfmtStep.Style.DROPBOX; +import static com.diffplug.spotless.kotlin.KtfmtStep.Style.META; -import java.io.IOException; import java.io.Serializable; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; +import java.util.Optional; -import com.diffplug.spotless.*; +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.ThrowingEx; /** - * Wraps up ktfmt as a FormatterStep. + * Wraps up ktfmt as a FormatterStep. */ -public class KtfmtStep { - // prevent direct instantiation - private KtfmtStep() {} +public class KtfmtStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String DEFAULT_VERSION = "0.53"; + private static final String NAME = "ktfmt"; + private static final String MAVEN_COORDINATE = "com.facebook:ktfmt:"; - private static final String DEFAULT_VERSION = "0.30"; - static final String NAME = "ktfmt"; - static final String PACKAGE = "com.facebook"; - static final String MAVEN_COORDINATE = PACKAGE + ":ktfmt:"; + private final String version; + /** + * Option that allows to apply formatting options to perform a 4-space block and continuation indent. + */ + @Nullable + private final Style style; + @Nullable + private final KtfmtFormattingOptions options; + /** The jar that contains the formatter. */ + private final JarState.Promised jarState; + + private KtfmtStep(String version, + JarState.Promised jarState, + @Nullable Style style, + @Nullable KtfmtFormattingOptions options) { + this.version = Objects.requireNonNull(version, "version"); + this.style = style; + this.options = options; + this.jarState = Objects.requireNonNull(jarState, "jarState"); + } /** * 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 { - DEFAULT("DEFAULT_FORMAT", "0.0"), DROPBOX("DROPBOX_FORMAT", "0.11"), GOOGLE("GOOGLE_FORMAT", "0.21"), KOTLINLANG("KOTLINLANG_FORMAT", "0.21"); + // @formatter:off + DEFAULT("DEFAULT_FORMAT", "0.0", "0.50"), + META("META_FORMAT", "0.51"), + DROPBOX("DROPBOX_FORMAT", "0.16", "0.50"), + GOOGLE("GOOGLE_FORMAT", "0.19"), + KOTLINLANG("KOTLINLANG_FORMAT", "0.21"), + ; + // @formatter:on final private String format; final private String since; + final private @Nullable String until; Style(String format, String since) { this.format = format; this.since = since; + this.until = null; + } + + Style(String format, String since, @Nullable String until) { + this.format = format; + this.since = since; + this.until = until; } String getFormat() { @@ -60,16 +102,68 @@ String getFormat() { String getSince() { return since; } + + /** Last version (inclusive) that supports this style */ + @Nullable + String getUntil() { + return until; + } } - private static final String DROPBOX_STYLE_METHOD = "dropboxStyle"; + public static class KtfmtFormattingOptions implements Serializable { - /** - * The format method is available in the link below. - * - * @see ktfmt source - */ - static final String FORMATTER_METHOD = "format"; + private static final long serialVersionUID = 1L; + + @Nullable + private Integer maxWidth = null; + + @Nullable + private Integer blockIndent = null; + + @Nullable + private Integer continuationIndent = null; + + @Nullable + private Boolean removeUnusedImports = null; + + @Nullable + private Boolean manageTrailingCommas = null; + + public KtfmtFormattingOptions() {} + + public KtfmtFormattingOptions( + @Nullable Integer maxWidth, + @Nullable Integer blockIndent, + @Nullable Integer continuationIndent, + @Nullable Boolean removeUnusedImports, + @Nullable Boolean manageTrailingCommas) { + this.maxWidth = maxWidth; + this.blockIndent = blockIndent; + this.continuationIndent = continuationIndent; + this.removeUnusedImports = removeUnusedImports; + this.manageTrailingCommas = manageTrailingCommas; + } + + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + } + + public void setBlockIndent(int blockIndent) { + this.blockIndent = blockIndent; + } + + public void setContinuationIndent(int continuationIndent) { + this.continuationIndent = continuationIndent; + } + + public void setRemoveUnusedImports(boolean removeUnusedImports) { + this.removeUnusedImports = removeUnusedImports; + } + + public void setManageTrailingCommas(boolean manageTrailingCommas) { + this.manageTrailingCommas = manageTrailingCommas; + } + } /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(Provisioner provisioner) { @@ -78,86 +172,255 @@ public static FormatterStep create(Provisioner provisioner) { /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(String version, Provisioner provisioner) { - return create(version, provisioner, DEFAULT); + return create(version, provisioner, null, null); } /** Creates a step which formats everything - code, import order, and unused imports. */ - public static FormatterStep create(String version, Provisioner provisioner, Style style) { + public static FormatterStep create(String version, Provisioner provisioner, @Nullable Style style, @Nullable KtfmtFormattingOptions options) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); - Objects.requireNonNull(style, "style"); - return FormatterStep.createLazy( - NAME, () -> new State(version, provisioner, style), State::createFormat); + return FormatterStep.create(NAME, + new KtfmtStep(version, JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), style, options), + KtfmtStep::equalityState, + State::createFormat); } public static String defaultVersion() { return DEFAULT_VERSION; } - public static String defaultStyle() { - return Style.DEFAULT.name(); + private State equalityState() { + return new State(version, jarState.get(), style, options); } - static final class State implements Serializable { + private static final class State implements Serializable { private static final long serialVersionUID = 1L; - private final String version; + @Nullable + private final Style style; + @Nullable + private final KtfmtFormattingOptions options; + private final JarState jarState; + + State(String version, + JarState jarState, + @Nullable Style style, + @Nullable KtfmtFormattingOptions options) { + this.version = version; + this.options = options; + this.style = style; + this.jarState = jarState; + validateStyle(); + validateOptions(); + } + + 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(); + } + + final Class formatterFuncClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtFormatterFunc"); + final Class ktfmtStyleClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtStyle"); + final Class ktfmtFormattingOptionsClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtFormattingOptions"); + + if (style == null && options == null) { + final Constructor constructor = formatterFuncClass.getConstructor(); + return (FormatterFunc) constructor.newInstance(); + } + + final Object ktfmtStyle = style == null ? null : Enum.valueOf((Class) ktfmtStyleClass, getKtfmtStyleOption(style)); + if (options == null) { + final Constructor constructor = formatterFuncClass.getConstructor(ktfmtStyleClass); + return (FormatterFunc) constructor.newInstance(ktfmtStyle); + } + + final Constructor optionsConstructor = ktfmtFormattingOptionsClass.getConstructor( + Integer.class, Integer.class, Integer.class, Boolean.class, Boolean.class); + final Object ktfmtFormattingOptions = optionsConstructor.newInstance( + options.maxWidth, options.blockIndent, options.continuationIndent, options.removeUnusedImports, options.manageTrailingCommas); + if (style == null) { + final Constructor constructor = formatterFuncClass.getConstructor(ktfmtFormattingOptionsClass); + return (FormatterFunc) constructor.newInstance(ktfmtFormattingOptions); + } + + final Constructor constructor = formatterFuncClass.getConstructor(ktfmtStyleClass, ktfmtFormattingOptionsClass); + return (FormatterFunc) constructor.newInstance(ktfmtStyle, ktfmtFormattingOptions); + } + + private void validateOptions() { + if (BadSemver.version(version) < BadSemver.version(0, 11)) { + if (options != null) { + throw new IllegalStateException("Ktfmt formatting options supported for version 0.11 and later"); + } + return; + } + + if (BadSemver.version(version) < BadSemver.version(0, 17)) { + if (options != null && options.removeUnusedImports != null) { + throw new IllegalStateException("Ktfmt formatting option `removeUnusedImports` supported for version 0.17 and later"); + } + } + } + + private void validateStyle() { + if (style == null) { + return; + } + + if (BadSemver.version(version) < BadSemver.version(style.since)) { + throw new IllegalStateException(String.format("The style %s is available from version %s (current version: %s)", style.name(), style.since, version)); + } + if (style.until != null && BadSemver.version(version) > BadSemver.version(style.until)) { + throw new IllegalStateException(String.format("The style %s is no longer available from version %s (current version: %s)", style.name(), style.until, version)); + } + } + + /** + * @param style + * @return com.diffplug.spotless.glue.ktfmt.KtfmtStyle enum value name + */ + private String getKtfmtStyleOption(Style style) { + switch (style) { + case META: + return "META"; + case GOOGLE: + return "GOOGLE"; + case KOTLINLANG: + return "KOTLIN_LANG"; + default: + throw new IllegalStateException("Unsupported style: " + style); + } + } + } + + private static final class KtfmtFormatterFuncCompat { + private static final String PACKAGE = "com.facebook.ktfmt"; - private final String pkg; /** - * Option that allows to apply formatting options to perform a 4 spaces block and continuation indent. + * The format method is available in the link below. + * + * @see ktfmt source */ + static final String FORMATTER_METHOD = "format"; + + private final String version; private final Style style; - /** The jar that contains the formatter. */ - final JarState jarState; + private final KtfmtFormattingOptions options; + private final ClassLoader classLoader; - State(String version, Provisioner provisioner, Style style) throws IOException { - this.version = version; - this.pkg = PACKAGE; + public KtfmtFormatterFuncCompat(String currentVersion, @Nullable Style style, @Nullable KtfmtFormattingOptions options, ClassLoader classLoader) { + this.version = currentVersion; this.style = style; - this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner); + this.options = options; + this.classLoader = classLoader; } - FormatterFunc createFormat() throws Exception { - ClassLoader classLoader = jarState.getClassLoader(); - Class formatterClazz = classLoader.loadClass(pkg + ".ktfmt.FormatterKt"); + public FormatterFunc getFormatterFunc() { return input -> { try { - if (style == DEFAULT) { - Method formatterMethod = formatterClazz.getMethod(FORMATTER_METHOD, String.class); - return (String) formatterMethod.invoke(formatterClazz, input); - } else { - Class formattingOptionsClazz = classLoader.loadClass(pkg + ".ktfmt.FormattingOptions"); - Method formatterMethod = formatterClazz.getMethod(FORMATTER_METHOD, formattingOptionsClazz, - String.class); - Object formattingOptions = getCustomFormattingOptions(classLoader, style); - return (String) formatterMethod.invoke(formatterClazz, formattingOptions, input); - } + return applyFormat(input); } catch (InvocationTargetException e) { throw ThrowingEx.unwrapCause(e); } }; } - private Object getCustomFormattingOptions(ClassLoader classLoader, Style style) throws Exception { - if (BadSemver.version(version) < BadSemver.version(style.since)) { - throw new IllegalStateException(String.format("The style %s is available from version %s (current version: %s)", style.name(), style.since, version)); + protected String applyFormat(String input) throws Exception { + Class formatterClass = getFormatterClazz(); + if (style == null && options == null || style == DEFAULT) { + Method formatterMethod = formatterClass.getMethod(FORMATTER_METHOD, String.class); + return (String) formatterMethod.invoke(formatterClass, input); + } else { + Method formatterMethod = formatterClass.getMethod(FORMATTER_METHOD, getFormattingOptionsClazz(), String.class); + Object formattingOptions = getCustomFormattingOptions(formatterClass); + return (String) formatterMethod.invoke(formatterClass, formattingOptions, input); + } + } + + private Object getCustomFormattingOptions(Class formatterClass) throws Exception { + Object formattingOptions = getFormattingOptionsFromStyle(formatterClass); + Class formattingOptionsClass = formattingOptions.getClass(); + + if (options != null) { + if (BadSemver.version(version) < BadSemver.version(0, 17)) { + formattingOptions = formattingOptions.getClass().getConstructor(int.class, int.class, int.class).newInstance( + /* maxWidth = */ Optional.ofNullable(options.maxWidth).orElse((Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), + /* blockIndent = */ Optional.ofNullable(options.blockIndent).orElse((Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), + /* continuationIndent = */ Optional.ofNullable(options.continuationIndent).orElse((Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions))); + } else if (BadSemver.version(version) < BadSemver.version(0, 19)) { + formattingOptions = formattingOptions.getClass().getConstructor(int.class, int.class, int.class, boolean.class, boolean.class).newInstance( + /* maxWidth = */ Optional.ofNullable(options.maxWidth).orElse((Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), + /* blockIndent = */ Optional.ofNullable(options.blockIndent).orElse((Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), + /* continuationIndent = */ Optional.ofNullable(options.continuationIndent).orElse((Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), + /* removeUnusedImports = */ Optional.ofNullable(options.removeUnusedImports).orElse((Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), + /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); + } else if (BadSemver.version(version) < BadSemver.version(0, 47)) { + Class styleClass = classLoader.loadClass(formattingOptionsClass.getName() + "$Style"); + formattingOptions = formattingOptions.getClass().getConstructor(styleClass, int.class, int.class, int.class, boolean.class, boolean.class).newInstance( + /* style = */ formattingOptionsClass.getMethod("getStyle").invoke(formattingOptions), + /* maxWidth = */ Optional.ofNullable(options.maxWidth).orElse((Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), + /* blockIndent = */ Optional.ofNullable(options.blockIndent).orElse((Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), + /* continuationIndent = */ Optional.ofNullable(options.continuationIndent).orElse((Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), + /* removeUnusedImports = */ Optional.ofNullable(options.removeUnusedImports).orElse((Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), + /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); + } else { + Class styleClass = classLoader.loadClass(formattingOptionsClass.getName() + "$Style"); + formattingOptions = formattingOptions.getClass().getConstructor(styleClass, int.class, int.class, int.class, boolean.class, boolean.class, boolean.class).newInstance( + /* style = */ formattingOptionsClass.getMethod("getStyle").invoke(formattingOptions), + /* maxWidth = */ Optional.ofNullable(options.maxWidth).orElse((Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), + /* blockIndent = */ Optional.ofNullable(options.blockIndent).orElse((Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), + /* continuationIndent = */ Optional.ofNullable(options.continuationIndent).orElse((Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), + /* removeUnusedImports = */ Optional.ofNullable(options.removeUnusedImports).orElse((Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), + /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions), + /* manageTrailingCommas */ Optional.ofNullable(options.manageTrailingCommas).orElse((Boolean) formattingOptionsClass.getMethod("getManageTrailingCommas").invoke(formattingOptions))); + } } - try { - // ktfmt v0.19 and later - return classLoader.loadClass(pkg + ".ktfmt.FormatterKt").getField(style.getFormat()).get(null); - } catch (NoSuchFieldException ignored) {} + return formattingOptions; + } - // fallback to old, pre-0.19 ktfmt interface. - if (style == Style.DEFAULT || style == Style.DROPBOX) { - Class formattingOptionsCompanionClazz = classLoader.loadClass(pkg + ".ktfmt.FormattingOptions$Companion"); + private Object getFormattingOptionsFromStyle(Class formatterClass) throws Exception { + Style style = this.style; + if (style == null) { + if (BadSemver.version(version) < BadSemver.version(0, 51)) { + style = DEFAULT; + } else { + style = META; + } + } + if (BadSemver.version(version) < BadSemver.version(0, 19)) { + if (style != DROPBOX) { + throw new IllegalStateException("Invalid style " + style + " for version " + version); + } + Class formattingOptionsCompanionClazz = classLoader.loadClass(PACKAGE + ".FormattingOptions$Companion"); Object companion = formattingOptionsCompanionClazz.getConstructors()[0].newInstance((Object) null); - Method formattingOptionsMethod = formattingOptionsCompanionClazz.getDeclaredMethod(DROPBOX_STYLE_METHOD); + Method formattingOptionsMethod = formattingOptionsCompanionClazz.getDeclaredMethod("dropboxStyle"); return formattingOptionsMethod.invoke(companion); } else { - throw new IllegalStateException("Versions pre-0.19 can only use Default and Dropbox styles"); + return formatterClass.getField(style.getFormat()).get(null); + } + } + + private Class getFormatterClazz() throws Exception { + Class formatterClazz; + if (BadSemver.version(version) >= BadSemver.version(0, 31)) { + formatterClazz = classLoader.loadClass(PACKAGE + ".format.Formatter"); + } else { + formatterClazz = classLoader.loadClass(PACKAGE + ".FormatterKt"); + } + return formatterClazz; + } + + private Class getFormattingOptionsClazz() throws Exception { + Class formattingOptionsClazz; + if (BadSemver.version(version) >= BadSemver.version(0, 31)) { + formattingOptionsClazz = classLoader.loadClass(PACKAGE + ".format.FormattingOptions"); + } else { + formattingOptionsClazz = classLoader.loadClass(PACKAGE + ".FormattingOptions"); } + return formattingOptionsClazz; } } } 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 19550b6e44..359e5b5de0 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,13 +25,17 @@ import com.diffplug.spotless.Provisioner; /** A step for flexmark-java. */ -public class FlexmarkStep { - // prevent direct instantiation - private FlexmarkStep() {} - - private static final String DEFAULT_VERSION = "0.62.2"; - private static final String NAME = "flexmark-java"; +public class FlexmarkStep implements Serializable { + 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"; + + private final JarState.Promised jarState; + + private FlexmarkStep(JarState.Promised jarState) { + this.jarState = jarState; + } /** Creates a formatter step for the default version. */ public static FormatterStep create(Provisioner provisioner) { @@ -42,8 +46,9 @@ public static FormatterStep create(Provisioner provisioner) { public static FormatterStep create(String version, Provisioner provisioner) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(JarState.from(MAVEN_COORDINATE + version, provisioner)), + return FormatterStep.create(NAME, + new FlexmarkStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner))), + FlexmarkStep::equalityState, State::createFormat); } @@ -51,11 +56,14 @@ public static String defaultVersion() { return DEFAULT_VERSION; } + private State equalityState() { + return new State(jarState.get()); + } + private static class State implements Serializable { private static final long serialVersionUID = 1L; - /** The jar that contains the formatter. */ - final JarState jarState; + private final JarState jarState; State(JarState jarState) { this.jarState = jarState; @@ -67,6 +75,5 @@ FormatterFunc createFormat() throws Exception { final Constructor constructor = formatterFunc.getConstructor(); return (FormatterFunc) constructor.newInstance(); } - } } 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 2190af88e7..4786474657 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,42 +19,66 @@ import java.io.Serializable; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Objects; import java.util.TreeMap; import java.util.function.Consumer; -import java.util.logging.Logger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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; -import com.diffplug.spotless.ThrowingEx.Supplier; /** A step for FreshMark. */ -public class FreshMarkStep { - // prevent direct instantiation - private FreshMarkStep() {} +public class FreshMarkStep implements Serializable { + 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:"; + + private static final String NASHORN_MAVEN_COORDINATE = "org.openjdk.nashorn:nashorn-core:"; + + private static final String NASHORN_VERSION = "15.4"; private static final String FORMATTER_CLASS = "com.diffplug.freshmark.FreshMark"; private static final String FORMATTER_METHOD = "compile"; + private final JarState.Promised jarState; + private final Map properties; + + private FreshMarkStep(JarState.Promised jarState, Map properties) { + this.jarState = jarState; + this.properties = properties; + } + /** Creates a formatter step for the given version and settings file. */ - public static FormatterStep create(Supplier> properties, Provisioner provisioner) { + public static FormatterStep create(Map properties, Provisioner provisioner) { return create(defaultVersion(), properties, provisioner); } /** Creates a formatter step for the given version and settings file. */ - public static FormatterStep create(String version, Supplier> properties, Provisioner provisioner) { + public static FormatterStep create(String version, Map properties, Provisioner provisioner) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(properties, "properties"); Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(JarState.from(MAVEN_COORDINATE + version, provisioner), properties.get()), + + List mavenCoordinates = new ArrayList<>(); + mavenCoordinates.add(MAVEN_COORDINATE + version); + if (Jvm.version() >= 15) { + mavenCoordinates.add("com.diffplug.jscriptbox:jscriptbox:3.0.1"); + mavenCoordinates.add(NASHORN_MAVEN_COORDINATE + NASHORN_VERSION); + } + + return FormatterStep.create(NAME, + new FreshMarkStep(JarState.promise(() -> JarState.from(mavenCoordinates, provisioner)), properties), + FreshMarkStep::equalityState, State::createFormat); } @@ -62,12 +86,15 @@ public static String defaultVersion() { return DEFAULT_VERSION; } + private State equalityState() throws Exception { + return new State(jarState.get(), properties); + } + private static class State implements Serializable { private static final long serialVersionUID = 1L; - /** The jar that contains the formatter. */ - final JarState jarState; - final NavigableMap properties; + private final JarState jarState; + private final NavigableMap properties; State(JarState jarState, Map properties) { this.jarState = jarState; @@ -78,8 +105,8 @@ private static class State implements Serializable { } FormatterFunc createFormat() throws Exception { - Logger logger = Logger.getLogger(FreshMarkStep.class.getName()); - Consumer loggingStream = logger::warning; + Logger logger = LoggerFactory.getLogger(FreshMarkStep.class); + Consumer loggingStream = logger::warn; ClassLoader classLoader = jarState.getClassLoader(); diff --git a/testlib/src/main/java/com/diffplug/spotless/JreVersion.java b/lib/src/main/java/com/diffplug/spotless/npm/BaseNpmRestService.java similarity index 64% rename from testlib/src/main/java/com/diffplug/spotless/JreVersion.java rename to lib/src/main/java/com/diffplug/spotless/npm/BaseNpmRestService.java index 829945896b..a6d93182a1 100644 --- a/testlib/src/main/java/com/diffplug/spotless/JreVersion.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/BaseNpmRestService.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless; +package com.diffplug.spotless.npm; -public class JreVersion { - private JreVersion() {} +abstract class BaseNpmRestService { - /** - * @return the major version of this VM, e.g. 8, 9, 10, 11, 13, etc. - * @deprecated Use {@link com.diffplug.spotless.Jvm#version()} instead. - */ - public static int thisVm() { - return Jvm.version(); + protected final SimpleRestClient restClient; + + BaseNpmRestService(String baseUrl) { + this.restClient = SimpleRestClient.forBaseUrl(baseUrl); + } + + public String shutdown() { + return restClient.post("/shutdown"); } + } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintConfig.java new file mode 100644 index 0000000000..8db9cf07af --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2024 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 java.io.File; +import java.io.Serializable; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FileSignature; + +public class EslintConfig implements Serializable { + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + private final FileSignature.Promised eslintConfigPathSignature; + private final String eslintConfigJs; + + public EslintConfig(@Nullable File eslintConfigPath, @Nullable String eslintConfigJs) { + this.eslintConfigPathSignature = eslintConfigPath == null ? null : FileSignature.promise(eslintConfigPath); + this.eslintConfigJs = eslintConfigJs; + } + + public EslintConfig withEslintConfigPath(@Nullable File eslintConfigPath) { + return new EslintConfig(eslintConfigPath, this.eslintConfigJs); + } + + @Nullable + public File getEslintConfigPath() { + return eslintConfigPathSignature == null ? null : eslintConfigPathSignature.get().getOnlyFile(); + } + + @Nullable + public String getEslintConfigJs() { + return eslintConfigJs; + } + + public EslintConfig verify() { + if (eslintConfigPathSignature == null && eslintConfigJs == null) { + throw new IllegalArgumentException("ESLint must be configured using either a configFile or a configJs - but both are null."); + } + return this; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java new file mode 100644 index 0000000000..70beaf91ca --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java @@ -0,0 +1,185 @@ +/* + * Copyright 2016-2024 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 java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterFunc.Closeable; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.ThrowingEx; +import com.diffplug.spotless.npm.EslintRestService.FormatOption; + +public class EslintFormatterStep { + + private static final Logger logger = LoggerFactory.getLogger(EslintFormatterStep.class); + + public static final String NAME = "eslint-format"; + + public static final String DEFAULT_ESLINT_VERSION = "^8.45.0"; + + public static Map defaultDevDependenciesForTypescript() { + return defaultDevDependenciesTypescriptWithEslint(DEFAULT_ESLINT_VERSION); + } + + public static Map defaultDevDependenciesTypescriptWithEslint(String eslintVersion) { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("@typescript-eslint/eslint-plugin", "^6.1.0"); + dependencies.put("@typescript-eslint/parser", "^6.1.0"); + dependencies.put("typescript", "^5.1.6"); + dependencies.put("eslint", Objects.requireNonNull(eslintVersion)); + return dependencies; + } + + public static Map defaultDevDependencies() { + return defaultDevDependenciesWithEslint(DEFAULT_ESLINT_VERSION); + } + + public static Map defaultDevDependenciesWithEslint(String version) { + return Collections.singletonMap("eslint", version); + } + + public static FormatterStep create(Map devDependencies, Provisioner provisioner, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) { + requireNonNull(devDependencies); + requireNonNull(provisioner); + requireNonNull(projectDir); + requireNonNull(buildDir); + return FormatterStep.createLazy(NAME, + () -> new State(NAME, devDependencies, projectDir, buildDir, cacheDir, npmPathResolver, eslintConfig), + State::createFormatterFunc); + } + + private static class State extends NpmFormatterStepStateBase implements Serializable { + private static final long serialVersionUID = 1L; + + private final EslintConfig origEslintConfig; + private EslintConfig eslintConfigInUse; + + State(String stepName, Map devDependencies, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, EslintConfig eslintConfig) throws IOException { + super(stepName, + new NpmConfig( + replaceDevDependencies( + NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, "/com/diffplug/spotless/npm/eslint-package.json"), + new TreeMap<>(devDependencies)), + NpmResourceHelper.readUtf8StringFromClasspath(EslintFormatterStep.class, + "/com/diffplug/spotless/npm/common-serve.js", + "/com/diffplug/spotless/npm/eslint-serve.js"), + npmPathResolver.resolveNpmrcContent()), + new NpmFormatterStepLocations( + projectDir, + buildDir, + cacheDir, + npmPathResolver)); + this.origEslintConfig = requireNonNull(eslintConfig.verify()); + this.eslintConfigInUse = eslintConfig; + } + + @Override + protected void prepareNodeServerLayout(NodeServerLayout nodeServerLayout) throws IOException { + super.prepareNodeServerLayout(nodeServerLayout); + if (origEslintConfig.getEslintConfigPath() != null) { + // If any config files are provided, we need to make sure they are at the same location as the node modules + // as eslint will try to resolve plugin/config names relatively to the config file location and some + // eslint configs contain relative paths to additional config files (such as tsconfig.json e.g.) + logger.debug("Copying config file <{}> to <{}> and using the copy", origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir()); + File configFileCopy = NpmResourceHelper.copyFileToDir(origEslintConfig.getEslintConfigPath(), nodeServerLayout.nodeModulesDir()); + this.eslintConfigInUse = this.origEslintConfig.withEslintConfigPath(configFileCopy).verify(); + } + } + + @Override + @Nonnull + public FormatterFunc createFormatterFunc() { + try { + logger.info("Creating formatter function (starting server)"); + Runtime runtime = toRuntime(); + ServerProcessInfo eslintRestServer = runtime.npmRunServer(); + EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl()); + return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), runtime.nodeServerLayout().nodeModulesDir(), eslintConfigInUse, restService)); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + private void endServer(BaseNpmRestService restService, ServerProcessInfo restServer) throws Exception { + logger.info("Closing formatting function (ending server)."); + try { + restService.shutdown(); + } catch (Throwable t) { + logger.info("Failed to request shutdown of rest service via api. Trying via process.", t); + } + restServer.close(); + } + + } + + private static class EslintFilePathPassingFormatterFunc implements FormatterFunc.NeedsFile { + private final File projectDir; + private final File nodeModulesDir; + private final EslintConfig eslintConfig; + private final EslintRestService restService; + + public EslintFilePathPassingFormatterFunc(File projectDir, File nodeModulesDir, EslintConfig eslintConfig, EslintRestService restService) { + this.projectDir = requireNonNull(projectDir); + this.nodeModulesDir = requireNonNull(nodeModulesDir); + this.eslintConfig = requireNonNull(eslintConfig); + this.restService = requireNonNull(restService); + } + + @Override + public String applyWithFile(String unix, File file) throws Exception { + Map eslintCallOptions = new HashMap<>(); + setConfigToCallOptions(eslintCallOptions); + setFilePathToCallOptions(eslintCallOptions, file); + return restService.format(unix, eslintCallOptions); + } + + private void setFilePathToCallOptions(Map eslintCallOptions, File fileToBeFormatted) { + eslintCallOptions.put(FormatOption.FILE_PATH, fileToBeFormatted.getAbsolutePath()); + } + + private void setConfigToCallOptions(Map eslintCallOptions) { + if (eslintConfig.getEslintConfigPath() != null) { + eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG_FILE, eslintConfig.getEslintConfigPath().getAbsolutePath()); + } + if (eslintConfig.getEslintConfigJs() != null) { + eslintCallOptions.put(FormatOption.ESLINT_OVERRIDE_CONFIG, eslintConfig.getEslintConfigJs()); + } + if (eslintConfig instanceof EslintTypescriptConfig) { + // if we are a ts config, see if we need to use specific paths or use default projectDir + File tsConfigFilePath = ((EslintTypescriptConfig) eslintConfig).getTypescriptConfigPath(); + File tsConfigRootDir = tsConfigFilePath != null ? tsConfigFilePath.getParentFile() : projectDir; + eslintCallOptions.put(FormatOption.TS_CONFIG_ROOT_DIR, nodeModulesDir.getAbsoluteFile().toPath().relativize(tsConfigRootDir.getAbsoluteFile().toPath()).toString()); + } + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintRestService.java new file mode 100644 index 0000000000..e17237ecbf --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintRestService.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2023 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 java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +public class EslintRestService extends BaseNpmRestService { + + EslintRestService(String baseUrl) { + super(baseUrl); + } + + public String format(String fileContent, Map formatOptions) { + Map jsonProperties = new LinkedHashMap<>(); + jsonProperties.put("file_content", fileContent); + for (Entry option : formatOptions.entrySet()) { + jsonProperties.put(option.getKey().backendName, option.getValue()); + } + return restClient.postJson("/eslint/format", jsonProperties); + } + + enum FormatOption { + ESLINT_OVERRIDE_CONFIG("eslint_override_config"), ESLINT_OVERRIDE_CONFIG_FILE("eslint_override_config_file"), FILE_PATH("file_path"), TS_CONFIG_ROOT_DIR("ts_config_root_dir"); + + private final String backendName; + + FormatOption(String backendName) { + this.backendName = backendName; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintTypescriptConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintTypescriptConfig.java new file mode 100644 index 0000000000..42dec6837a --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintTypescriptConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022-2024 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 java.io.File; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FileSignature; + +public class EslintTypescriptConfig extends EslintConfig { + private static final long serialVersionUID = 2L; + + @SuppressWarnings("unused") + private final FileSignature.Promised typescriptConfigPathSignature; + + public EslintTypescriptConfig(@Nullable File eslintConfigPath, @Nullable String eslintConfigJs, @Nullable File typescriptConfigPath) { + super(eslintConfigPath, eslintConfigJs); + this.typescriptConfigPathSignature = typescriptConfigPath != null ? FileSignature.promise(typescriptConfigPath) : null; + } + + @Override + public EslintConfig withEslintConfigPath(@Nullable File eslintConfigPath) { + return new EslintTypescriptConfig(eslintConfigPath, this.getEslintConfigJs(), getTypescriptConfigPath()); + } + + @Nullable + public File getTypescriptConfigPath() { + return typescriptConfigPathSignature == null ? null : this.typescriptConfigPathSignature.get().getOnlyFile(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/FileFinder.java b/lib/src/main/java/com/diffplug/spotless/npm/FileFinder.java index 76016d68db..fce1497de6 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/FileFinder.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/FileFinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2023 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 @@ class FileFinder { private final List>> fileCandidateFinders; private FileFinder(Builder builder) { - this.fileCandidateFinders = Collections.unmodifiableList(new ArrayList<>(builder.candidateFinders)); + this.fileCandidateFinders = List.copyOf(builder.candidateFinders); } static Builder finderForFilename(String fileName) { diff --git a/lib/src/main/java/com/diffplug/spotless/npm/FormattedPrinter.java b/lib/src/main/java/com/diffplug/spotless/npm/FormattedPrinter.java deleted file mode 100644 index 97d5cdb4c6..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/FormattedPrinter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 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 java.util.Objects.requireNonNull; - -import java.io.PrintStream; -import java.time.LocalDateTime; - -enum FormattedPrinter { - SYSOUT(System.out); - - private static final boolean enabled = false; - - private final PrintStream printStream; - - FormattedPrinter(PrintStream printStream) { - this.printStream = requireNonNull(printStream); - } - - public void print(String msg, Object... paramsForStringFormat) { - if (!enabled) { - return; - } - String formatted = String.format(msg, paramsForStringFormat); - String prefixed = String.format("[%s] %s", LocalDateTime.now().toString(), formatted); - this.printStream.println(prefixed); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java index 163818d0e7..848dd030dd 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,22 @@ public static String jsonEscape(Object val) { if (val instanceof String) { return jsonEscape((String) val); } + if (ListableAdapter.canAdapt(val)) { + // create an array + StringBuilder sb = new StringBuilder(); + sb.append('['); + boolean first = true; + for (Object o : ListableAdapter.adapt(val)) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(jsonEscape(o)); + } + sb.append(']'); + return sb.toString(); + } return val.toString(); } @@ -44,7 +60,7 @@ private static String jsonEscape(JsonRawValue jsonRawValue) { private static String jsonEscape(String unescaped) { /** * the following characters are reserved in JSON and must be properly escaped to be used in strings: - * + *

* Backspace is replaced with \b * Form feed is replaced with \f * Newline is replaced with \n @@ -52,7 +68,7 @@ private static String jsonEscape(String unescaped) { * Tab is replaced with \t * Double quote is replaced with \" * Backslash is replaced with \\ - * + *

* additionally we handle xhtml '' string * and non-ascii chars */ diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonWriter.java similarity index 77% rename from lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java rename to lib/src/main/java/com/diffplug/spotless/npm/JsonWriter.java index 846f7f1cf3..bbdf9b63d4 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,33 +28,46 @@ import com.diffplug.spotless.ThrowingEx; -public class SimpleJsonWriter { +class JsonWriter { private final LinkedHashMap valueMap = new LinkedHashMap<>(); - public static SimpleJsonWriter of(Map values) { - SimpleJsonWriter writer = new SimpleJsonWriter(); + public static JsonWriter of(Map values) { + JsonWriter writer = new JsonWriter(); writer.putAll(values); return writer; } - SimpleJsonWriter putAll(Map values) { + JsonWriter putAll(Map values) { verifyValues(values); this.valueMap.putAll(values); return this; } - SimpleJsonWriter put(String name, Object value) { + JsonWriter put(String name, Object value) { verifyValues(Collections.singletonMap(name, value)); this.valueMap.put(name, value); return this; } private void verifyValues(Map values) { - if (values.values() - .stream() - .anyMatch(val -> !(val instanceof String || val instanceof JsonRawValue || val instanceof Number || val instanceof Boolean))) { - throw new IllegalArgumentException("Only values of type 'String', 'JsonRawValue', 'Number' and 'Boolean' are supported. You provided: " + values.values()); + for (Object value : values.values()) { + verifyValue(value); + } + } + + private void verifyValue(Object val) { + if (val == null) { + return; + } + if (ListableAdapter.canAdapt(val)) { + for (Object o : ListableAdapter.adapt(val)) { + verifyValue(o); + } + return; + } + if (!(val instanceof String || val instanceof JsonRawValue || val instanceof Number || val instanceof Boolean)) { + throw new IllegalArgumentException("Only values of type 'String', 'JsonRawValue', 'Number' and 'Boolean' are supported. You provided: " + val); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/ListableAdapter.java b/lib/src/main/java/com/diffplug/spotless/npm/ListableAdapter.java new file mode 100644 index 0000000000..24ae62fde6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/ListableAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 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 java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nonnull; + +class ListableAdapter implements Iterable { + + private final List delegate; + + @SuppressWarnings("unchecked") + private ListableAdapter(Object delegate) { + Objects.requireNonNull(delegate); + if (!canAdapt(delegate)) { + throw new IllegalArgumentException("Cannot create ListableAdapter from " + delegate.getClass() + ". Use canAdapt() to check first."); + } + if (delegate instanceof List) { + this.delegate = (List) delegate; + } else if (delegate.getClass().isArray()) { + this.delegate = Arrays.asList((T[]) delegate); + } else { + throw new IllegalArgumentException("Cannot create IterableAdapter from " + delegate.getClass()); + } + } + + static Iterable adapt(Object delegate) { + return new ListableAdapter<>(delegate); + } + + @Override + @Nonnull + public Iterator iterator() { + return delegate.iterator(); + } + + static boolean canAdapt(Object delegate) { + Objects.requireNonNull(delegate); + return delegate instanceof List || delegate.getClass().isArray(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeApp.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeApp.java new file mode 100644 index 0000000000..19eb584c57 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeApp.java @@ -0,0 +1,125 @@ +/* + * Copyright 2023-2025 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 com.diffplug.spotless.npm.NpmProcessFactory.OnlinePreferrence.PREFER_OFFLINE; +import static com.diffplug.spotless.npm.NpmProcessFactory.OnlinePreferrence.PREFER_ONLINE; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.ProcessRunner; + +public class NodeApp { + + private static final Logger logger = LoggerFactory.getLogger(NodeApp.class); + + private static final TimedLogger timedLogger = TimedLogger.forLogger(logger); + + @Nonnull + protected final NodeServerLayout nodeServerLayout; + + @Nonnull + protected final NpmConfig npmConfig; + + @Nonnull + protected final NpmProcessFactory npmProcessFactory; + + @Nonnull + protected final NpmFormatterStepLocations formatterStepLocations; + + public NodeApp(@Nonnull NodeServerLayout nodeServerLayout, @Nonnull NpmConfig npmConfig, @Nonnull NpmFormatterStepLocations formatterStepLocations) { + this.nodeServerLayout = Objects.requireNonNull(nodeServerLayout); + this.npmConfig = Objects.requireNonNull(npmConfig); + this.npmProcessFactory = processFactory(formatterStepLocations); + this.formatterStepLocations = Objects.requireNonNull(formatterStepLocations); + } + + private static NpmProcessFactory processFactory(NpmFormatterStepLocations formatterStepLocations) { + if (formatterStepLocations.cacheDir() != null) { + logger.info("Caching npm install results in {}.", formatterStepLocations.cacheDir()); + return NodeModulesCachingNpmProcessFactory.create(formatterStepLocations.cacheDir()); + } + logger.debug("Not caching npm install results."); + return StandardNpmProcessFactory.INSTANCE; + } + + boolean needsNpmInstall() { + return !this.nodeServerLayout.isNodeModulesPrepared(); + } + + boolean needsPrepareNodeAppLayout() { + return !this.nodeServerLayout.isLayoutPrepared(); + } + + void prepareNodeAppLayout() { + timedLogger.withInfo("Preparing {} for npm step {}.", this.nodeServerLayout, getClass().getName()).run(() -> { + NpmResourceHelper.assertDirectoryExists(nodeServerLayout.nodeModulesDir()); + NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.packageJsonFile(), this.npmConfig.getPackageJsonContent()); + if (this.npmConfig.getServeScriptContent() != null) { + NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.serveJsFile(), this.npmConfig.getServeScriptContent()); + } else { + NpmResourceHelper.deleteFileIfExists(nodeServerLayout.serveJsFile()); + } + if (this.npmConfig.getNpmrcContent() != null) { + NpmResourceHelper.writeUtf8StringToFile(nodeServerLayout.npmrcFile(), this.npmConfig.getNpmrcContent()); + } else { + NpmResourceHelper.deleteFileIfExists(nodeServerLayout.npmrcFile()); + } + }); + } + + void npmInstall() { + timedLogger.withInfo("Installing npm dependencies for {} with {}.", this.nodeServerLayout, this.npmProcessFactory.describe()) + .run(this::optimizedNpmInstall); + } + + private void optimizedNpmInstall() { + try { + npmProcessFactory.createNpmInstallProcess(nodeServerLayout, formatterStepLocations, PREFER_OFFLINE).waitFor(); + } catch (NpmProcessException e) { + if (!offlineInstallFailed(e.getResult())) { + throw e; // pass through + } + // if the npm install fails in a way that might be caused by missing dependency information, we try again without the offline flag + npmProcessFactory.createNpmInstallProcess(nodeServerLayout, formatterStepLocations, PREFER_ONLINE).waitFor(); + } + } + + private static boolean offlineInstallFailed(ProcessRunner.Result result) { + if (result == null) { + return false; // no result, something else must have happened + } + if (result.exitCode() == 0) { + return false; // all is well + } + var installOutput = result.stdOutUtf8(); + // offline install failed, needs online install + return isNoMatchingVersionFound(installOutput) || isCannotResolveDependencyTree(installOutput); + } + + private static boolean isNoMatchingVersionFound(String installOutput) { + return installOutput.contains("code ETARGET") && installOutput.contains("No matching version found for"); + } + + private static boolean isCannotResolveDependencyTree(String installOutput) { + return installOutput.contains("code ERESOLVE") && installOutput.contains("unable to resolve dependency tree"); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java new file mode 100644 index 0000000000..eb30ae78b8 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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 java.io.File; +import java.util.Optional; + +class NodeExecutableResolver { + + private NodeExecutableResolver() { + // no instance + } + + static String nodeExecutableName() { + String nodeName = "node"; + if (PlatformInfo.normalizedOS() == PlatformInfo.OS.WINDOWS) { + nodeName += ".exe"; + } + return nodeName; + } + + static Optional tryFindNextTo(File npmExecutable) { + if (npmExecutable == null) { + return Optional.empty(); + } + File nodeExecutable = new File(npmExecutable.getParentFile(), nodeExecutableName()); + if (nodeExecutable.exists() && nodeExecutable.isFile() && nodeExecutable.canExecute()) { + return Optional.of(nodeExecutable); + } + return Optional.empty(); + } + + public static String explainMessage() { + return "Spotless was unable to find a node executable.\n" + + "Either specify the node executable explicitly or make sure it can be found next to the npm executable."; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java new file mode 100644 index 0000000000..2f1addf2af --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeModulesCachingNpmProcessFactory.java @@ -0,0 +1,119 @@ +/* + * Copyright 2023-2024 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 java.io.File; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.ProcessRunner.Result; + +public class NodeModulesCachingNpmProcessFactory implements NpmProcessFactory { + + private static final Logger logger = LoggerFactory.getLogger(NodeModulesCachingNpmProcessFactory.class); + + private static final TimedLogger timedLogger = TimedLogger.forLogger(logger); + + private final File cacheDir; + + private final ShadowCopy shadowCopy; + + private NodeModulesCachingNpmProcessFactory(@Nonnull File cacheDir) { + this.cacheDir = Objects.requireNonNull(cacheDir); + assertDir(); // throws if cacheDir is not a directory + 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."); + } + } + return cacheDir; + } + + public static NodeModulesCachingNpmProcessFactory create(@Nonnull File cacheDir) { + return new NodeModulesCachingNpmProcessFactory(cacheDir); + } + + @Override + public NpmProcess createNpmInstallProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations, OnlinePreferrence onlinePreferrence) { + NpmProcess actualNpmInstallProcess = StandardNpmProcessFactory.INSTANCE.createNpmInstallProcess(nodeServerLayout, formatterStepLocations, onlinePreferrence); + return new CachingNmpInstall(actualNpmInstallProcess, nodeServerLayout); + } + + @Override + public NpmLongRunningProcess createNpmServeProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations) { + return StandardNpmProcessFactory.INSTANCE.createNpmServeProcess(nodeServerLayout, formatterStepLocations); + } + + private class CachingNmpInstall implements NpmProcess { + + private final NpmProcess actualNpmInstallProcess; + private final NodeServerLayout nodeServerLayout; + + public CachingNmpInstall(NpmProcess actualNpmInstallProcess, NodeServerLayout nodeServerLayout) { + this.actualNpmInstallProcess = actualNpmInstallProcess; + this.nodeServerLayout = nodeServerLayout; + } + + @Override + public Result waitFor() { + String entryName = entryName(); + if (shadowCopy.entryExists(entryName, NodeServerLayout.NODE_MODULES)) { + timedLogger.withInfo("Using cached node_modules for {} from {}", entryName, cacheDir) + .run(() -> shadowCopy.copyEntryInto(entryName(), NodeServerLayout.NODE_MODULES, nodeServerLayout.nodeModulesDir())); + return new CachedResult(); + } else { + Result result = timedLogger.withInfo("calling actual npm install {}", actualNpmInstallProcess.describe()) + .call(actualNpmInstallProcess::waitFor); + assert result.exitCode() == 0; + storeShadowCopy(entryName); + return result; + } + } + + private void storeShadowCopy(String entryName) { + timedLogger.withInfo("Caching node_modules for {} in {}", entryName, cacheDir) + .run(() -> shadowCopy.addEntry(entryName(), new File(nodeServerLayout.nodeModulesDir(), NodeServerLayout.NODE_MODULES))); + } + + private String entryName() { + return nodeServerLayout.nodeModulesDir().getName(); + } + + @Override + public String describe() { + return String.format("Wrapper around [%s] to cache node_modules in [%s]", actualNpmInstallProcess.describe(), cacheDir.getAbsolutePath()); + } + } + + private class CachedResult extends Result { + + public CachedResult() { + super(List.of("(from cache dir " + cacheDir + ")"), 0, new byte[0], new byte[0]); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeServeApp.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeServeApp.java new file mode 100644 index 0000000000..c9311a5589 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeServeApp.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.ProcessRunner; + +public class NodeServeApp extends NodeApp { + + private static final Logger logger = LoggerFactory.getLogger(NodeApp.class); + + private static final TimedLogger timedLogger = TimedLogger.forLogger(logger); + + public NodeServeApp(@Nonnull NodeServerLayout nodeServerLayout, @Nonnull NpmConfig npmConfig, @Nonnull NpmFormatterStepLocations formatterStepLocations) { + super(nodeServerLayout, npmConfig, formatterStepLocations); + } + + ProcessRunner.LongRunningProcess startNpmServeProcess() { + return timedLogger.withInfo("Starting npm based server in {} with {}.", this.nodeServerLayout.nodeModulesDir(), this.npmProcessFactory.describe()) + .call(() -> npmProcessFactory.createNpmServeProcess(nodeServerLayout, formatterStepLocations).start()); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java index a175080517..850ea4eb6b 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeServerLayout.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,45 @@ package com.diffplug.spotless.npm; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import com.diffplug.spotless.ThrowingEx; class NodeServerLayout { + private static final Pattern PACKAGE_JSON_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\""); + static final String NODE_MODULES = "node_modules"; + private final File nodeModulesDir; private final File packageJsonFile; + + private final File packageLockJsonFile; + private final File serveJsFile; private final File npmrcFile; - NodeServerLayout(File buildDir, String stepName) { - this.nodeModulesDir = new File(buildDir, "spotless-node-modules-" + stepName); + NodeServerLayout(File buildDir, String packageJsonContent) { + this.nodeModulesDir = new File(buildDir, nodeModulesDirName(packageJsonContent)); this.packageJsonFile = new File(nodeModulesDir, "package.json"); + this.packageLockJsonFile = new File(nodeModulesDir, "package-lock.json"); this.serveJsFile = new File(nodeModulesDir, "serve.js"); this.npmrcFile = new File(nodeModulesDir, ".npmrc"); } + private static String nodeModulesDirName(String packageJsonContent) { + String md5Hash = NpmResourceHelper.md5(packageJsonContent); + Matcher matcher = PACKAGE_JSON_NAME_PATTERN.matcher(packageJsonContent); + if (!matcher.find()) { + throw new IllegalArgumentException("package.json must contain a name property"); + } + String packageName = matcher.group(1); + return String.format("%s-node-modules-%s", packageName, md5Hash); + } + File nodeModulesDir() { return nodeModulesDir; } @@ -47,7 +71,43 @@ public File npmrcFile() { return npmrcFile; } - static File getBuildDirFromNodeModulesDir(File nodeModulesDir) { - return nodeModulesDir.getParentFile(); + public boolean isLayoutPrepared() { + if (!nodeModulesDir().isDirectory()) { + return false; + } + if (!packageJsonFile().isFile()) { + return false; + } + if (!packageLockJsonFile.isFile()) { + return false; + } + if (!serveJsFile().isFile()) { + return false; + } + // npmrc is optional, so must not be checked here + return true; + } + + public boolean isNodeModulesPrepared() { + Path nodeModulesInstallDirPath = new File(nodeModulesDir(), NODE_MODULES).toPath(); + if (!Files.isDirectory(nodeModulesInstallDirPath)) { + return false; + } + // check if it is NOT empty + return ThrowingEx.get(() -> { + try (Stream entries = Files.list(nodeModulesInstallDirPath)) { + return entries.findFirst().isPresent(); + } + }); + } + + @Override + public String toString() { + return String.format( + "NodeServerLayout[nodeModulesDir=%s, packageJsonFile=%s, serveJsFile=%s, npmrcFile=%s]", + this.nodeModulesDir, + this.packageJsonFile, + this.serveJsFile, + this.npmrcFile); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java index 1492fe7a99..863be41193 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 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,7 @@ package com.diffplug.spotless.npm; import java.io.Serializable; +import java.util.Objects; import javax.annotation.Nonnull; @@ -23,30 +24,24 @@ class NpmConfig implements Serializable { private static final long serialVersionUID = 684264546497914877L; + @Nonnull private final String packageJsonContent; - private final String npmModule; - private final String serveScriptContent; private final String npmrcContent; - public NpmConfig(String packageJsonContent, String npmModule, String serveScriptContent, String npmrcContent) { - this.packageJsonContent = packageJsonContent; - this.npmModule = npmModule; + public NpmConfig(@Nonnull String packageJsonContent, String serveScriptContent, String npmrcContent) { + this.packageJsonContent = Objects.requireNonNull(packageJsonContent); this.serveScriptContent = serveScriptContent; this.npmrcContent = npmrcContent; } + @Nonnull public String getPackageJsonContent() { return packageJsonContent; } - public String getNpmModule() { - return npmModule; - } - - @Nonnull public String getServeScriptContent() { return serveScriptContent; } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java new file mode 100644 index 0000000000..4df5736dd9 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023-2024 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 java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.Serializable; + +import javax.annotation.Nonnull; + +class NpmFormatterStepLocations implements Serializable { + + private static final long serialVersionUID = -1055408537924029969L; + + private final File projectDir; + private final File buildDir; + private final File cacheDir; + private final NpmPathResolver resolver; + + public NpmFormatterStepLocations(@Nonnull File projectDir, @Nonnull File buildDir, File cacheDir, @Nonnull NpmPathResolver resolver) { + this.projectDir = requireNonNull(projectDir); + this.buildDir = requireNonNull(buildDir); + this.cacheDir = cacheDir; + this.resolver = requireNonNull(resolver); + } + + public File projectDir() { + return projectDir; + } + + public File buildDir() { + return buildDir; + } + + public File cacheDir() { + return cacheDir; + } + + public File npmExecutable() { + return resolver.resolveNpmExecutable(); + } + + public File nodeExecutable() { + return resolver.resolveNodeExecutable(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java index 48bec00c8f..3ec06e0434 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,97 +27,116 @@ import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.logging.Logger; -import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.FormatterFunc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.ProcessRunner.LongRunningProcess; +import com.diffplug.spotless.ThrowingEx; abstract class NpmFormatterStepStateBase implements Serializable { - private static final Logger logger = Logger.getLogger(NpmFormatterStepStateBase.class.getName()); - - private static final long serialVersionUID = 1460749955865959948L; + private static final Logger logger = LoggerFactory.getLogger(NpmFormatterStepStateBase.class); - @SuppressWarnings("unused") - private final FileSignature packageJsonSignature; + private static final TimedLogger timedLogger = TimedLogger.forLogger(logger); - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - public final transient File nodeModulesDir; - - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - private final transient File npmExecutable; + private static final long serialVersionUID = 1460749955865959948L; + private final String stepName; private final NpmConfig npmConfig; - private final String stepName; + public final NpmFormatterStepLocations locations; - protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir, File npm) throws IOException { + protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, NpmFormatterStepLocations locations) throws IOException { this.stepName = requireNonNull(stepName); this.npmConfig = requireNonNull(npmConfig); - this.npmExecutable = npm; - - NodeServerLayout layout = prepareNodeServer(buildDir); - this.nodeModulesDir = layout.nodeModulesDir(); - this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile()); + this.locations = locations; } - private NodeServerLayout prepareNodeServer(File buildDir) throws IOException { - NodeServerLayout layout = new NodeServerLayout(buildDir, stepName); - NpmResourceHelper.assertDirectoryExists(layout.nodeModulesDir()); - NpmResourceHelper.writeUtf8StringToFile(layout.packageJsonFile(), - this.npmConfig.getPackageJsonContent()); - NpmResourceHelper - .writeUtf8StringToFile(layout.serveJsFile(), this.npmConfig.getServeScriptContent()); - if (this.npmConfig.getNpmrcContent() != null) { - NpmResourceHelper.writeUtf8StringToFile(layout.npmrcFile(), this.npmConfig.getNpmrcContent()); - } else { - NpmResourceHelper.deleteFileIfExists(layout.npmrcFile()); - } - FormattedPrinter.SYSOUT.print("running npm install"); - runNpmInstall(layout.nodeModulesDir()); - FormattedPrinter.SYSOUT.print("npm install finished"); - return layout; + public Runtime toRuntime() { + return new Runtime(this); } - private void runNpmInstall(File npmProjectDir) throws IOException { - new NpmProcess(npmProjectDir, this.npmExecutable).install(); + protected void prepareNodeServerLayout(NodeServerLayout layout) throws IOException { + } - protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException { - if (!this.nodeModulesDir.exists()) { - prepareNodeServer(NodeServerLayout.getBuildDirFromNodeModulesDir(this.nodeModulesDir)); + public static class Runtime { + private final NpmFormatterStepStateBase parent; + private final NodeServerLayout nodeServerLayout; + private final NodeServeApp nodeServeApp; + + Runtime(NpmFormatterStepStateBase parent) { + this.parent = parent; + this.nodeServerLayout = new NodeServerLayout(parent.locations.buildDir(), parent.npmConfig.getPackageJsonContent()); + this.nodeServeApp = new NodeServeApp(nodeServerLayout, parent.npmConfig, parent.locations); + } + + public NodeServerLayout nodeServerLayout() { + return nodeServerLayout; } - try { - // The npm process will output the randomly selected port of the http server process to 'server.port' file - // so in order to be safe, remove such a file if it exists before starting. - final File serverPortFile = new File(this.nodeModulesDir, "server.port"); - NpmResourceHelper.deleteFileIfExists(serverPortFile); - // start the http server in node - Process server = new NpmProcess(this.nodeModulesDir, this.npmExecutable).start(); + protected void prepareNodeServerLayout() throws IOException { + nodeServeApp.prepareNodeAppLayout(); + parent.prepareNodeServerLayout(nodeServerLayout); + } + + protected void prepareNodeServer() throws IOException { + nodeServeApp.npmInstall(); + } + + protected void assertNodeServerDirReady() throws IOException { + if (needsPrepareNodeServerLayout()) { + // reinstall if missing + prepareNodeServerLayout(); + } + if (needsPrepareNodeServer()) { + // run npm install if node_modules is missing + prepareNodeServer(); + } + } - // await the readiness of the http server - wait for at most 60 seconds + protected boolean needsPrepareNodeServer() { + return nodeServeApp.needsNpmInstall(); + } + + protected boolean needsPrepareNodeServerLayout() { + return nodeServeApp.needsPrepareNodeAppLayout(); + } + + protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException { + assertNodeServerDirReady(); + LongRunningProcess server = null; try { - NpmResourceHelper.awaitReadableFile(serverPortFile, Duration.ofSeconds(60)); - } catch (TimeoutException timeoutException) { - // forcibly end the server process + // The npm process will output the randomly selected port of the http server process to 'server.port' file + // so in order to be safe, remove such a file if it exists before starting. + final File serverPortFile = new File(this.nodeServerLayout.nodeModulesDir(), "server.port"); + NpmResourceHelper.deleteFileIfExists(serverPortFile); + // start the http server in node + server = nodeServeApp.startNpmServeProcess(); + + // await the readiness of the http server - wait for at most 60 seconds try { - if (server.isAlive()) { - server.destroyForcibly(); - server.waitFor(); + NpmResourceHelper.awaitReadableFile(serverPortFile, Duration.ofSeconds(60)); + } catch (TimeoutException timeoutException) { + // forcibly end the server process + try { + if (server.isAlive()) { + server.destroyForcibly(); + server.waitFor(); + } + } catch (Throwable t) { + // ignore } - } catch (Throwable t) { - // ignore + throw timeoutException; } - throw timeoutException; + // read the server.port file for resulting port and remember the port for later formatting calls + String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim(); + return new ServerProcessInfo(server, serverPort, serverPortFile); + } catch (IOException | TimeoutException e) { + throw new ServerStartException("Starting server failed." + (server != null ? "\n\nProcess result:\n" + ThrowingEx.get(server::result) : ""), e); } - // read the server.port file for resulting port and remember the port for later formatting calls - String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim(); - return new ServerProcessInfo(server, serverPort, serverPortFile); - } catch (IOException | TimeoutException e) { - throw new ServerStartException(e); } } @@ -166,13 +185,15 @@ public String getBaseUrl() { @Override public void close() throws Exception { try { - logger.fine("Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">"); + logger.trace("Closing npm server in directory <{}> and port <{}>", + serverPortFile.getParent(), serverPort); + if (server.isAlive()) { boolean ended = server.waitFor(5, TimeUnit.SECONDS); if (!ended) { - logger.info("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">"); + logger.info("Force-Closing npm server in directory <{}> and port <{}>", serverPortFile.getParent(), serverPort); server.destroyForcibly().waitFor(); - logger.fine("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + "> -- Finished"); + logger.trace("Force-Closing npm server in directory <{}> and port <{}> -- Finished", serverPortFile.getParent(), serverPort); } } } finally { @@ -184,8 +205,8 @@ public void close() throws Exception { protected static class ServerStartException extends RuntimeException { private static final long serialVersionUID = -8803977379866483002L; - public ServerStartException(Throwable cause) { - super(cause); + public ServerStartException(String message, Throwable cause) { + super(message, cause); } } } diff --git a/_ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/package-info.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmLongRunningProcess.java similarity index 72% rename from _ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/package-info.java rename to lib/src/main/java/com/diffplug/spotless/npm/NpmLongRunningProcess.java index 190557cc74..f5bece3e06 100644 --- a/_ext/eclipse-jdt/src/main/java/com/diffplug/spotless/extra/eclipse/java/package-info.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmLongRunningProcess.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Eclipse JDT based Spotless formatter */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.java; +package com.diffplug.spotless.npm; -import javax.annotation.ParametersAreNonnullByDefault; +import com.diffplug.spotless.ProcessRunner.LongRunningProcess; + +interface NpmLongRunningProcess { + + String describe(); + + LongRunningProcess start(); + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java index a9a15bd49d..f62340f074 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,28 +16,70 @@ package com.diffplug.spotless.npm; import java.io.File; -import java.util.Arrays; +import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import java.util.Optional; -public class NpmPathResolver { +public class NpmPathResolver implements Serializable { + private static final long serialVersionUID = 1L; private final File explicitNpmExecutable; + private final File explicitNodeExecutable; + private final File explicitNpmrcFile; private final List additionalNpmrcLocations; - public NpmPathResolver(File explicitNpmExecutable, File explicitNpmrcFile, File... additionalNpmrcLocations) { + public NpmPathResolver(File explicitNpmExecutable, File explicitNodeExecutable, File explicitNpmrcFile, List additionalNpmrcLocations) { this.explicitNpmExecutable = explicitNpmExecutable; + this.explicitNodeExecutable = explicitNodeExecutable; this.explicitNpmrcFile = explicitNpmrcFile; - this.additionalNpmrcLocations = Arrays.asList(additionalNpmrcLocations); + // We must not use an immutable list (e.g. List.copyOf) here, because immutable lists cannot be restored + // from Gradle’s serialisation. See https://github.com/diffplug/spotless/issues/2372 + this.additionalNpmrcLocations = new ArrayList<>(additionalNpmrcLocations); } + /** + * Finds the npm executable to use. + *
+ * Either the explicit npm executable is returned, or - if an explicit node executable is configured - tries to find + * the npm executable relative to the node executable. + * Falls back to looking for npm on the user's system using {@link NpmExecutableResolver} + * + * @return the npm executable to use + * @throws IllegalStateException if no npm executable could be found + */ public File resolveNpmExecutable() { - return Optional.ofNullable(this.explicitNpmExecutable) - .orElseGet(() -> NpmExecutableResolver.tryFind() - .orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage()))); + if (this.explicitNpmExecutable != null) { + return this.explicitNpmExecutable; + } + if (this.explicitNodeExecutable != null) { + File nodeExecutableCandidate = new File(this.explicitNodeExecutable.getParentFile(), NpmExecutableResolver.npmExecutableName()); + if (nodeExecutableCandidate.canExecute()) { + return nodeExecutableCandidate; + } + } + return NpmExecutableResolver.tryFind() + .orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage())); + } + + /** + * Finds the node executable to use. + *
+ * Either the explicit node executable is returned, or tries to find the node executable relative to the npm executable + * found by {@link #resolveNpmExecutable()}. + * @return the node executable to use + * @throws IllegalStateException if no node executable could be found + */ + public File resolveNodeExecutable() { + if (this.explicitNodeExecutable != null) { + return this.explicitNodeExecutable; + } + File npmExecutable = resolveNpmExecutable(); + return NodeExecutableResolver.tryFindNextTo(npmExecutable) + .orElseThrow(() -> new IllegalStateException("Can't automatically determine node executable and none was specifically supplied!\n\n" + NodeExecutableResolver.explainMessage())); } public String resolveNpmrcContent() { diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java index e1bf8c8673..473057a769 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,78 +15,12 @@ */ package com.diffplug.spotless.npm; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; +import com.diffplug.spotless.ProcessRunner.Result; -class NpmProcess { +interface NpmProcess { - private final File workingDir; + String describe(); - private final File npmExecutable; + Result waitFor(); - NpmProcess(File workingDir, File npmExecutable) { - this.workingDir = workingDir; - this.npmExecutable = npmExecutable; - } - - void install() { - npmAwait("install", "--no-audit", "--no-package-lock"); - } - - Process start() { - // adding --scripts-prepend-node-path=true due to https://github.com/diffplug/spotless/issues/619#issuecomment-648018679 - return npm("start", "--scripts-prepend-node-path=true"); - } - - private void npmAwait(String... args) { - final Process npmProcess = npm(args); - - try { - if (npmProcess.waitFor() != 0) { - throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue()); - } - } catch (InterruptedException e) { - throw new NpmProcessException("Running npm command '" + commandLine(args) + "' was interrupted.", e); - } - } - - private Process npm(String... args) { - List processCommand = processCommand(args); - try { - return new ProcessBuilder() - .inheritIO() - .directory(this.workingDir) - .command(processCommand) - .start(); - } catch (IOException e) { - throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e); - } - } - - private List processCommand(String... args) { - List command = new ArrayList<>(args.length + 1); - command.add(this.npmExecutable.getAbsolutePath()); - command.addAll(Arrays.asList(args)); - return command; - } - - private String commandLine(String... args) { - return "npm " + Arrays.stream(args).collect(Collectors.joining(" ")); - } - - static class NpmProcessException extends RuntimeException { - private static final long serialVersionUID = 6424331316676759525L; - - public NpmProcessException(String message) { - super(message); - } - - public NpmProcessException(String message, Throwable cause) { - super(message, cause); - } - } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessException.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessException.java new file mode 100644 index 0000000000..c2e8444bcc --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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 javax.annotation.CheckForNull; + +import com.diffplug.spotless.ProcessRunner; + +public class NpmProcessException extends RuntimeException { + private static final long serialVersionUID = 6424331316676759525L; + private final transient ProcessRunner.Result result; + + public NpmProcessException(String message, ProcessRunner.Result result) { + super(message); + this.result = result; + } + + public NpmProcessException(String message, Throwable cause) { + super(message, cause); + this.result = null; + } + + @CheckForNull + public ProcessRunner.Result getResult() { + return result; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessFactory.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessFactory.java new file mode 100644 index 0000000000..dbc9a807d5 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcessFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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; + +public interface NpmProcessFactory { + + enum OnlinePreferrence { + PREFER_OFFLINE("--prefer-offline"), PREFER_ONLINE("--prefer-online"); + + private final String option; + + OnlinePreferrence(String option) { + this.option = option; + } + + public String option() { + return option; + } + } + + NpmProcess createNpmInstallProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations, OnlinePreferrence onlinePreferrence); + + NpmLongRunningProcess createNpmServeProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations); + + default String describe() { + return getClass().getSimpleName(); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java index ad1211d717..7a28685de0 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,15 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; import java.time.Duration; +import java.util.Arrays; +import java.util.Objects; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import com.diffplug.spotless.ThrowingEx; @@ -45,6 +52,12 @@ static void deleteFileIfExists(File file) throws IOException { } } + static String readUtf8StringFromClasspath(Class clazz, String... resourceNames) { + return Arrays.stream(resourceNames) + .map(resourceName -> readUtf8StringFromClasspath(clazz, resourceName)) + .collect(Collectors.joining("\n")); + } + static String readUtf8StringFromClasspath(Class clazz, String resourceName) { try (InputStream input = clazz.getResourceAsStream(resourceName)) { return readUtf8StringFromInputStream(input); @@ -90,6 +103,52 @@ static void awaitReadableFile(File file, Duration maxWaitTime) throws TimeoutExc if ((System.currentTimeMillis() - startedAt) > maxWaitTime.toMillis()) { throw new TimeoutException("The file did not appear within " + maxWaitTime); } + ThrowingEx.run(() -> Thread.sleep(100)); + } + } + + static void awaitFileDeleted(File file, Duration maxWaitTime) throws TimeoutException { + final long startedAt = System.currentTimeMillis(); + while (file.exists()) { + // wait for at most maxWaitTime + if ((System.currentTimeMillis() - startedAt) > maxWaitTime.toMillis()) { + throw new TimeoutException("The file did not disappear within " + maxWaitTime); + } + ThrowingEx.run(() -> Thread.sleep(100)); + } + } + + static File copyFileToDir(File file, File targetDir) { + return copyFileToDirAtSubpath(file, targetDir, file.getName()); + } + + static File copyFileToDirAtSubpath(File file, File targetDir, String relativePath) { + Objects.requireNonNull(relativePath); + try { + // create file pointing to relativePath in targetDir + final Path relativeTargetFile = Paths.get(targetDir.getAbsolutePath(), relativePath); + assertDirectoryExists(relativeTargetFile.getParent().toFile()); + + Files.copy(file.toPath(), relativeTargetFile, StandardCopyOption.REPLACE_EXISTING); + return relativeTargetFile.toFile(); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + static String md5(File file) { + return md5(readUtf8StringFromFile(file)); + } + + static String md5(String fileContent) { + MessageDigest md = ThrowingEx.get(() -> MessageDigest.getInstance("MD5")); + md.update(fileContent.getBytes(StandardCharsets.UTF_8)); + byte[] digest = md.digest(); + // convert byte array digest to hex string + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xff)); } + return sb.toString(); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java index 328cb25a4e..8a51910205 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package com.diffplug.spotless.npm; import java.io.File; -import java.io.IOException; import java.io.Serializable; import java.util.Map; import java.util.TreeMap; @@ -24,36 +23,23 @@ import javax.annotation.Nullable; import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.ThrowingEx; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class PrettierConfig implements Serializable { private static final long serialVersionUID = -8709340269833126583L; - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - @Nullable - private final transient File prettierConfigPath; - - @SuppressWarnings("unused") - private final FileSignature prettierConfigPathSignature; + private final FileSignature.Promised prettierConfigPathSignature; private final TreeMap options; public PrettierConfig(@Nullable File prettierConfigPath, @Nullable Map options) { - try { - this.prettierConfigPath = prettierConfigPath; - this.prettierConfigPathSignature = prettierConfigPath != null ? FileSignature.signAsList(this.prettierConfigPath) : FileSignature.signAsList(); - this.options = options == null ? new TreeMap<>() : new TreeMap<>(options); - } catch (IOException e) { - throw ThrowingEx.asRuntime(e); - } + this.prettierConfigPathSignature = prettierConfigPath == null ? null : FileSignature.promise(prettierConfigPath); + this.options = options == null ? new TreeMap<>() : new TreeMap<>(options); } @Nullable public File getPrettierConfigPath() { - return prettierConfigPath; + return prettierConfigPathSignature == null ? null : prettierConfigPathSignature.get().getOnlyFile(); } public Map getOptions() { 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 0cd0f5f558..27a1002df5 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,12 @@ import java.util.Collections; import java.util.Map; import java.util.TreeMap; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterFunc.Closeable; import com.diffplug.spotless.FormatterStep; @@ -36,24 +37,26 @@ public class PrettierFormatterStep { - private static final Logger logger = Logger.getLogger(PrettierFormatterStep.class.getName()); + private static final Logger logger = LoggerFactory.getLogger(PrettierFormatterStep.class); public static final String NAME = "prettier-format"; + public static final String DEFAULT_VERSION = "2.8.8"; + public static final Map defaultDevDependencies() { - return defaultDevDependenciesWithPrettier("2.0.5"); + return defaultDevDependenciesWithPrettier(DEFAULT_VERSION); } public static final Map defaultDevDependenciesWithPrettier(String version) { return Collections.singletonMap("prettier", version); } - public static FormatterStep create(Map devDependencies, Provisioner provisioner, File buildDir, NpmPathResolver npmPathResolver, PrettierConfig prettierConfig) { + public static FormatterStep create(Map devDependencies, Provisioner provisioner, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, PrettierConfig prettierConfig) { requireNonNull(devDependencies); requireNonNull(provisioner); requireNonNull(buildDir); return FormatterStep.createLazy(NAME, - () -> new State(NAME, devDependencies, buildDir, npmPathResolver, prettierConfig), + () -> new State(NAME, devDependencies, projectDir, buildDir, cacheDir, npmPathResolver, prettierConfig), State::createFormatterFunc); } @@ -62,17 +65,21 @@ private static class State extends NpmFormatterStepStateBase implements Serializ private static final long serialVersionUID = -539537027004745812L; private final PrettierConfig prettierConfig; - State(String stepName, Map devDependencies, File buildDir, NpmPathResolver npmPathResolver, PrettierConfig prettierConfig) throws IOException { + State(String stepName, Map devDependencies, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, PrettierConfig prettierConfig) throws IOException { super(stepName, new NpmConfig( replaceDevDependencies( NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"), new TreeMap<>(devDependencies)), - "prettier", - NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-serve.js"), + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, + "/com/diffplug/spotless/npm/common-serve.js", + "/com/diffplug/spotless/npm/prettier-serve.js"), npmPathResolver.resolveNpmrcContent()), - buildDir, - npmPathResolver.resolveNpmExecutable()); + new NpmFormatterStepLocations( + projectDir, + buildDir, + cacheDir, + npmPathResolver)); this.prettierConfig = requireNonNull(prettierConfig); } @@ -80,8 +87,8 @@ private static class State extends NpmFormatterStepStateBase implements Serializ @Nonnull public FormatterFunc createFormatterFunc() { try { - FormattedPrinter.SYSOUT.print("creating formatter function (starting server)"); - ServerProcessInfo prettierRestServer = npmRunServer(); + logger.info("creating formatter function (starting server)"); + ServerProcessInfo prettierRestServer = toRuntime().npmRunServer(); PrettierRestService restService = new PrettierRestService(prettierRestServer.getBaseUrl()); String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions()); return Closeable.ofDangerous(() -> endServer(restService, prettierRestServer), new PrettierFilePathPassingFormatterFunc(prettierConfigOptions, restService)); @@ -91,11 +98,11 @@ public FormatterFunc createFormatterFunc() { } private void endServer(PrettierRestService restService, ServerProcessInfo restServer) throws Exception { - FormattedPrinter.SYSOUT.print("Closing formatting function (ending server)."); + logger.info("Closing formatting function (ending server)."); try { restService.shutdown(); } catch (Throwable t) { - logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t); + logger.info("Failed to request shutdown of rest service via api. Trying via process.", t); } restServer.close(); } @@ -113,10 +120,15 @@ public PrettierFilePathPassingFormatterFunc(String prettierConfigOptions, Pretti @Override public String applyWithFile(String unix, File file) throws Exception { - FormattedPrinter.SYSOUT.print("formatting String '" + unix.substring(0, Math.min(50, unix.length())) + "[...]' in file '" + file + "'"); - final String prettierConfigOptionsWithFilepath = assertFilepathInConfigOptions(file); - return restService.format(unix, prettierConfigOptionsWithFilepath); + try { + return restService.format(unix, prettierConfigOptionsWithFilepath); + } catch (SimpleRestClient.SimpleRestResponseException e) { + if (e.getStatusCode() != 200 && e.getResponseMessage().contains("No parser could be inferred")) { + throw new PrettierMissingParserException(file, e); + } + throw e; + } } private String assertFilepathInConfigOptions(File file) { @@ -135,4 +147,5 @@ private String assertFilepathInConfigOptions(File file) { return "{" + filePathOption + (hasAnyConfigOption ? "," : "") + prettierConfigOptions.substring(startOfConfigOption + 1); } } + } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java new file mode 100644 index 0000000000..9728b61eec --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java @@ -0,0 +1,125 @@ +/* + * Copyright 2023-2024 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 java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.Nonnull; + +import com.diffplug.spotless.Lint; + +class PrettierMissingParserException extends RuntimeException implements Lint.Has { + private static final long serialVersionUID = 1L; + + private static final Map EXTENSIONS_TO_PLUGINS; + + static { + Map plugins = new HashMap<>(); + // ---- official plugins + plugins.put(".php", "@prettier/plugin-php"); + plugins.put(".pug", "@prettier/plugin-pug"); + plugins.put(".rb", "@prettier/plugin-ruby"); + plugins.put(".xml", "@prettier/plugin-xml"); + + // ---- community plugins + // default namings: astro, elm, java, jsonata, prisma, properties, sh, sql, svelte, toml + plugins.put(".trigger", "prettier-plugin-apex"); + plugins.put(".cls", "prettier-plugin-apex"); + plugins.put(".html.erb", "prettier-plugin-erb"); + Arrays.asList(".glsl", + ".fp", + ".frag", + ".frg", + ".fs", + ".fsh", + ".fshader", + ".geo", + ".geom", + ".glslf", + ".glslv", + ".gs", + ".gshader", + ".rchit", + ".rmiss", + ".shader", + ".tesc", + ".tese", + ".vert", + ".vrx", + ".vsh", + ".vshader").forEach(ext -> plugins.put(ext, "prettier-plugin-glsl")); + Arrays.asList(".go.html", + ".gohtml", + ".gotmpl", + ".go.tmpl", + ".tmpl", + ".tpl", + ".html.tmpl", + ".html.tpl").forEach(ext -> plugins.put(ext, "prettier-plugin-go-template")); + plugins.put(".kt", "kotlin"); + plugins.put(".mo", "motoko"); + Arrays.asList(".nginx", ".nginxconf").forEach(ext -> plugins.put(ext, "prettier-plugin-nginx")); + plugins.put(".sol", "prettier-plugin-solidity"); + + EXTENSIONS_TO_PLUGINS = Collections.unmodifiableMap(plugins); + } + + private final File file; + + public PrettierMissingParserException(@Nonnull File file, Exception cause) { + super("Prettier could not infer a parser for file '" + file + "'. Maybe you need to include a prettier plugin in devDependencies?\n\n" + recommendPlugin(file), cause); + this.file = Objects.requireNonNull(file); + } + + @Override + public List getLints() { + return List.of(Lint.atUndefinedLine("no-parser", "Could not infer a parser. Maybe you need to include a prettier plugin in devDependencies? e.g. " + recommendPlugin(file))); + } + + private static String recommendPlugin(File file) { + String pluginName = guessPlugin(file); + return "A good candidate for file '" + file + "' is '" + pluginName + "\n" + + "See if you can find it on \n" + + "or search on npmjs.com for a plugin matching that name: " + + String.format("", pluginName) + + "\n\n" + + "For instructions on how to include plugins for prettier in spotless see our documentation:\n" + + "- for Gradle \n" + + "- for Maven "; + } + + private static String guessPlugin(File file) { + return EXTENSIONS_TO_PLUGINS.entrySet().stream() + .filter(entry -> file.getName().endsWith(entry.getKey())) + .findFirst() + .map(entry -> entry.getValue()) + .orElse("prettier-plugin-" + extension(file)); + } + + public String fileType() { + return extension(file); + } + + private static String extension(File file) { + return file.getName().substring(file.getName().lastIndexOf('.') + 1); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java index ced08b013f..11fd29c68a 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,10 @@ import java.util.LinkedHashMap; import java.util.Map; -public class PrettierRestService { - - private final SimpleRestClient restClient; +public class PrettierRestService extends BaseNpmRestService { PrettierRestService(String baseUrl) { - this.restClient = SimpleRestClient.forBaseUrl(baseUrl); + super(baseUrl); } public String resolveConfig(File prettierConfigPath, Map prettierConfigOptions) { @@ -33,8 +31,7 @@ public String resolveConfig(File prettierConfigPath, Map prettie jsonProperties.put("prettier_config_path", prettierConfigPath.getAbsolutePath()); } if (prettierConfigOptions != null) { - jsonProperties.put("prettier_config_options", SimpleJsonWriter.of(prettierConfigOptions).toJsonRawValue()); - + jsonProperties.put("prettier_config_options", JsonWriter.of(prettierConfigOptions).toJsonRawValue()); } return restClient.postJson("/prettier/config-options", jsonProperties); } @@ -48,9 +45,4 @@ public String format(String fileContent, String configOptionsJsonString) { return restClient.postJson("/prettier/format", jsonProperties); } - - public String shutdown() { - return restClient.post("/shutdown"); - } - } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/ShadowCopy.java b/lib/src/main/java/com/diffplug/spotless/npm/ShadowCopy.java new file mode 100644 index 0000000000..3182d81fbb --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/ShadowCopy.java @@ -0,0 +1,182 @@ +/* + * Copyright 2023-2024 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 java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystemException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.ThrowingEx; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +class ShadowCopy { + + private static final Logger logger = LoggerFactory.getLogger(ShadowCopy.class); + + private final Supplier shadowCopyRootSupplier; + + public ShadowCopy(@Nonnull Supplier shadowCopyRootSupplier) { + this.shadowCopyRootSupplier = shadowCopyRootSupplier; + } + + private File shadowCopyRoot() { + File shadowCopyRoot = shadowCopyRootSupplier.get(); + if (!shadowCopyRoot.isDirectory()) { + throw new IllegalStateException("Shadow copy root must be a directory: " + shadowCopyRoot); + } + return shadowCopyRoot; + } + + public void addEntry(String key, File orig) { + File target = entry(key, orig.getName()); + if (target.exists()) { + logger.debug("Shadow copy entry already exists, not overwriting: {}", key); + } else { + try { + storeEntry(key, orig, target); + } catch (Throwable ex) { + // Log but don't fail + logger.warn("Unable to store cache entry for {}", key, ex); + } + } + } + + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") + private void storeEntry(String key, File orig, File target) throws IOException { + // Create a temp directory in the same directory as target + Files.createDirectories(target.toPath().getParent()); + Path tempDirectory = Files.createTempDirectory(target.toPath().getParent(), key); + logger.debug("Will store entry {} to temporary directory {}, which is a sibling of the ultimate target {}", orig, tempDirectory, target); + + try { + // Copy orig to temp dir + Files.walkFileTree(orig.toPath(), new CopyDirectoryRecursively(tempDirectory, orig.toPath())); + try { + logger.debug("Finished storing entry {}. Atomically moving temporary directory {} into final place {}", key, tempDirectory, target); + // Atomically rename the completed cache entry into place + Files.move(tempDirectory, target.toPath(), StandardCopyOption.ATOMIC_MOVE); + } catch (FileAlreadyExistsException | DirectoryNotEmptyException e) { + // Someone already beat us to it + logger.debug("Shadow copy entry now exists, not overwriting: {}", key); + } catch (AtomicMoveNotSupportedException e) { + logger.warn("The filesystem at {} does not support atomic moves. Spotless cannot safely cache on such a system due to race conditions. Caching has been skipped.", target.toPath().getParent(), e); + } + } finally { + // Best effort to clean up + if (Files.exists(tempDirectory)) { + try { + Files.walkFileTree(tempDirectory, new DeleteDirectoryRecursively()); + } catch (Throwable ex) { + logger.warn("Ignoring error while cleaning up temporary copy", ex); + } + } + } + } + + public File getEntry(String key, String fileName) { + return entry(key, fileName); + } + + private File entry(String key, String origName) { + return Paths.get(shadowCopyRoot().getAbsolutePath(), key, origName).toFile(); + } + + public File copyEntryInto(String key, String origName, File targetParentFolder) { + File target = Paths.get(targetParentFolder.getAbsolutePath(), origName).toFile(); + if (target.exists()) { + logger.warn("Shadow copy destination already exists, deleting! {}: {}", key, target); + ThrowingEx.run(() -> Files.walkFileTree(target.toPath(), new DeleteDirectoryRecursively())); + } + // copy directory "orig" to "target" using hard links if possible or a plain copy otherwise + ThrowingEx.run(() -> Files.walkFileTree(entry(key, origName).toPath(), new CopyDirectoryRecursively(target.toPath(), entry(key, origName).toPath()))); + return target; + } + + public boolean entryExists(String key, String origName) { + return entry(key, origName).exists(); + } + + private static class CopyDirectoryRecursively extends SimpleFileVisitor { + private final Path target; + private final Path orig; + + private boolean tryHardLink = true; + + public CopyDirectoryRecursively(Path target, Path orig) { + this.target = target; + this.orig = orig; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + // create directory on target + Files.createDirectories(target.resolve(orig.relativize(dir))); + return super.preVisitDirectory(dir, attrs); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + // first try to hardlink, if that fails, copy + if (tryHardLink) { + try { + Files.createLink(target.resolve(orig.relativize(file)), file); + return super.visitFile(file, attrs); + } catch (UnsupportedOperationException | SecurityException | FileSystemException e) { + logger.debug("Shadow copy entry does not support hard links: {}. Switching to 'copy'.", file, e); + tryHardLink = false; // remember that hard links are not supported + } catch (IOException e) { + logger.debug("Shadow copy entry failed to create hard link: {}. Switching to 'copy'.", file, e); + tryHardLink = false; // remember that hard links are not supported + } + } + // copy file to target + Files.copy(file, target.resolve(orig.relativize(file))); + return super.visitFile(file, attrs); + } + } + + // https://stackoverflow.com/questions/3775694/deleting-folder-from-java + private static class DeleteDirectoryRecursively extends SimpleFileVisitor { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return super.visitFile(file, attrs); + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return super.postVisitDirectory(dir, exc); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java index 561839c757..2f413e4b3a 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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 @@ static SimpleRestClient forBaseUrl(String baseUrl) { } String postJson(String endpoint, Map jsonParams) throws SimpleRestException { - final SimpleJsonWriter jsonWriter = SimpleJsonWriter.of(jsonParams); + final JsonWriter jsonWriter = JsonWriter.of(jsonParams); final String jsonString = jsonWriter.toJsonString(); return postJson(endpoint, jsonString); diff --git a/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java b/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java new file mode 100644 index 0000000000..91cf80ad69 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/StandardNpmProcessFactory.java @@ -0,0 +1,144 @@ +/* + * Copyright 2023 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 java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import com.diffplug.spotless.ProcessRunner; + +public class StandardNpmProcessFactory implements NpmProcessFactory { + + public static final StandardNpmProcessFactory INSTANCE = new StandardNpmProcessFactory(); + + private StandardNpmProcessFactory() { + // only one instance neeeded + } + + @Override + public NpmProcess createNpmInstallProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations, OnlinePreferrence onlinePreferrence) { + return new NpmInstall(nodeServerLayout.nodeModulesDir(), formatterStepLocations, onlinePreferrence); + } + + @Override + public NpmLongRunningProcess createNpmServeProcess(NodeServerLayout nodeServerLayout, NpmFormatterStepLocations formatterStepLocations) { + return new NpmServe(nodeServerLayout.nodeModulesDir(), formatterStepLocations); + } + + private static abstract class AbstractStandardNpmProcess { + protected final ProcessRunner processRunner = ProcessRunner.usingRingBuffersOfCapacity(100 * 1024); // 100kB + + protected final File workingDir; + protected final NpmFormatterStepLocations formatterStepLocations; + + public AbstractStandardNpmProcess(File workingDir, NpmFormatterStepLocations formatterStepLocations) { + this.formatterStepLocations = formatterStepLocations; + this.workingDir = workingDir; + } + + protected String npmExecutable() { + return formatterStepLocations.npmExecutable().getAbsolutePath(); + } + + protected abstract List commandLine(); + + protected Map environmentVariables() { + return Map.of( + "PATH", formatterStepLocations.nodeExecutable().getParentFile().getAbsolutePath() + File.pathSeparator + System.getenv("PATH")); + } + + protected ProcessRunner.LongRunningProcess doStart() { + try { + return processRunner.start(workingDir, environmentVariables(), null, true, commandLine()); + } catch (IOException e) { + throw new NpmProcessException("Failed to launch npm command '" + describe() + "'.", e); + } + } + + protected abstract String describe(); + + public String doDescribe() { + return String.format("%s in %s [%s]", getClass().getSimpleName(), workingDir, String.join(" ", commandLine())); + } + } + + private static class NpmInstall extends AbstractStandardNpmProcess implements NpmProcess { + + private final OnlinePreferrence onlinePreferrence; + + public NpmInstall(File workingDir, NpmFormatterStepLocations formatterStepLocations, OnlinePreferrence onlinePreferrence) { + super(workingDir, formatterStepLocations); + this.onlinePreferrence = onlinePreferrence; + } + + @Override + protected List commandLine() { + return List.of( + npmExecutable(), + "install", + "--no-audit", + "--no-fund", + onlinePreferrence.option()); + } + + @Override + public String describe() { + return doDescribe(); + } + + @Override + public ProcessRunner.Result waitFor() { + try (ProcessRunner.LongRunningProcess npmProcess = doStart()) { + if (npmProcess.waitFor() != 0) { + throw new NpmProcessException("Running npm command '" + describe() + "' failed with exit code: " + npmProcess.exitValue() + "\n\n" + npmProcess.result(), npmProcess.result()); + } + return npmProcess.result(); + } catch (InterruptedException e) { + throw new NpmProcessException("Running npm command '" + describe() + "' was interrupted.", e); + } catch (ExecutionException e) { + throw new NpmProcessException("Running npm command '" + describe() + "' failed.", e); + } + } + } + + private static class NpmServe extends AbstractStandardNpmProcess implements NpmLongRunningProcess { + + public NpmServe(File workingDir, NpmFormatterStepLocations formatterStepLocations) { + super(workingDir, formatterStepLocations); + } + + @Override + protected List commandLine() { + return List.of( + npmExecutable(), + "start", + "--scripts-prepend-node-path=true"); + } + + @Override + public String describe() { + return doDescribe(); + } + + @Override + public ProcessRunner.LongRunningProcess start() { + return doStart(); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TimedLogger.java b/lib/src/main/java/com/diffplug/spotless/npm/TimedLogger.java new file mode 100644 index 0000000000..537234fcee --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TimedLogger.java @@ -0,0 +1,228 @@ +/* + * Copyright 2023 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 com.diffplug.spotless.LazyArgLogger.lazy; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; + +import com.diffplug.spotless.ThrowingEx; + +/** + * A logger that logs the time it took to execute a block of code. + */ +class TimedLogger { + + public static final String MESSAGE_PREFIX_BEGIN = "[BEGIN] "; + + public static final String MESSAGE_PREFIX_END = "[END] "; + + public static final String MESSAGE_SUFFIX_TOOK = " (took {})"; + + private final Logger logger; + private final Ticker ticker; + + private TimedLogger(@Nonnull Logger logger, Ticker ticker) { + this.logger = Objects.requireNonNull(logger); + this.ticker = ticker; + } + + public static TimedLogger forLogger(@Nonnull Logger logger) { + return forLogger(logger, Ticker.systemTicker()); + } + + public static TimedLogger forLogger(@Nonnull Logger logger, Ticker ticker) { + return new TimedLogger(logger, ticker); + } + + public TimedExec withInfo(@Nonnull String message, Object... args) { + return new TimedExec(logger::isInfoEnabled, logger::info, ticker, message, args); + } + + public TimedExec withDebug(@Nonnull String message, Object... args) { + return new TimedExec(logger::isDebugEnabled, logger::debug, ticker, message, args); + } + + public TimedExec withTrace(@Nonnull String message, Object... args) { + return new TimedExec(logger::isTraceEnabled, logger::trace, ticker, message, args); + } + + public TimedExec withWarn(@Nonnull String message, Object... args) { + return new TimedExec(logger::isWarnEnabled, logger::warn, ticker, message, args); + } + + public TimedExec withError(@Nonnull String message, Object... args) { + return new TimedExec(logger::isErrorEnabled, logger::error, ticker, message, args); + } + + public static class Timed implements AutoCloseable { + + @Nonnull + private final String msg; + + @Nonnull + private final List params; + @Nonnull + private final LogToLevelMethod delegatedLogger; + @Nonnull + private final Ticker ticker; + + private final long startedAt; + + public Timed(@Nonnull Ticker ticker, @Nonnull String msg, @Nonnull List params, @Nonnull LogToLevelMethod delegatedLogger) { + this.ticker = Objects.requireNonNull(ticker); + this.msg = Objects.requireNonNull(msg); + this.params = List.copyOf(Objects.requireNonNull(params)); + this.delegatedLogger = Objects.requireNonNull(delegatedLogger); + this.startedAt = ticker.read(); + logStart(); + } + + private void logStart() { + delegatedLogger.log(MESSAGE_PREFIX_BEGIN + msg, params.toArray()); + } + + private void logEnd() { + delegatedLogger.log(MESSAGE_PREFIX_END + msg + MESSAGE_SUFFIX_TOOK, paramsForEnd()); + } + + @Override + public final void close() { + logEnd(); + } + + private Object[] paramsForEnd() { + if (params.isEmpty() || !(params.get(params.size() - 1) instanceof Throwable)) { + // if the last element is not a throwable, we can add the duration as the last element + return Stream.concat(params.stream(), Stream.of(lazy(this::durationString))).toArray(); + } + // if the last element is a throwable, we have to add the duration before the last element + return Stream.concat( + params.stream().limit(params.size() - 1), + Stream.of(lazy(this::durationString), + params.get(params.size() - 1))) + .toArray(); + } + + private String durationString() { + long duration = ticker.read() - startedAt; + if (duration < 1000) { + return duration + "ms"; + } else if (duration < 1000 * 60) { + long seconds = duration / 1000; + long millis = duration - seconds * 1000; + return seconds + "." + millis + "s"; + } else { + // output in the format 3m 4.321s + long minutes = duration / (1000 * 60); + long seconds = (duration - minutes * 1000 * 60) / 1000; + long millis = duration - minutes * 1000 * 60 - seconds * 1000; + return minutes + "m" + (seconds + millis > 0 ? " " + seconds + "." + millis + "s" : ""); + } + } + } + + public static final class NullStopWatchLogger extends Timed { + private static final NullStopWatchLogger INSTANCE = new NullStopWatchLogger(); + + private NullStopWatchLogger() { + super(Ticker.systemTicker(), "", List.of(), (m, a) -> {}); + } + } + + interface Ticker { + long read(); + + static Ticker systemTicker() { + return System::currentTimeMillis; + } + } + + static class TestTicker implements Ticker { + private long time = 0; + + @Override + public long read() { + return time; + } + + public void tickMillis(long millis) { + time += millis; + } + } + + public static class TimedExec { + @Nonnull + private final LogActiveMethod logActiveMethod; + @Nonnull + private final LogToLevelMethod logMethod; + @Nonnull + private final Ticker ticker; + @Nonnull + private final String message; + @Nonnull + private final Object[] args; + + public TimedExec(LogActiveMethod logActiveMethod, LogToLevelMethod logMethod, Ticker ticker, String message, Object... args) { + this.logActiveMethod = Objects.requireNonNull(logActiveMethod); + this.logMethod = Objects.requireNonNull(logMethod); + this.ticker = Objects.requireNonNull(ticker); + this.message = Objects.requireNonNull(message); + this.args = Objects.requireNonNull(args); + } + + public void run(ThrowingEx.Runnable r) { + try (Timed ignore = timed()) { + ThrowingEx.run(r); + } + } + + public T call(ThrowingEx.Supplier s) { + try (Timed ignore = timed()) { + return ThrowingEx.get(s); + } + } + + public void runChecked(ThrowingEx.Runnable r) throws Exception { + try (Timed ignore = timed()) { + r.run(); + } + } + + private Timed timed() { + if (logActiveMethod.isLogLevelActive()) { + return new Timed(ticker, message, List.of(args), logMethod); + } + return NullStopWatchLogger.INSTANCE; + } + } + + @FunctionalInterface + private interface LogActiveMethod { + boolean isLogLevelActive(); + } + + @FunctionalInterface + private interface LogToLevelMethod { + void log(String message, Object... args); + } +} 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 a87bc0ebae..b171b0ca7f 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,13 @@ import java.io.IOException; import java.io.Serializable; import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterFunc.Closeable; import com.diffplug.spotless.FormatterStep; @@ -35,15 +36,15 @@ public class TsFmtFormatterStep { - private static final Logger logger = Logger.getLogger(TsFmtFormatterStep.class.getName()); + private static final Logger logger = LoggerFactory.getLogger(TsFmtFormatterStep.class); public static final String NAME = "tsfmt-format"; - public static FormatterStep create(Map versions, Provisioner provisioner, File buildDir, NpmPathResolver npmPathResolver, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) { + public static FormatterStep create(Map versions, Provisioner provisioner, File projectDir, File buildDir, File cacheDir, NpmPathResolver npmPathResolver, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) { requireNonNull(provisioner); requireNonNull(buildDir); return FormatterStep.createLazy(NAME, - () -> new State(NAME, versions, buildDir, npmPathResolver, configFile, inlineTsFmtSettings), + () -> new State(NAME, versions, projectDir, buildDir, cacheDir, npmPathResolver, configFile, inlineTsFmtSettings), State::createFormatterFunc); } @@ -70,15 +71,19 @@ public static class State extends NpmFormatterStepStateBase implements Serializa @Nullable private final TypedTsFmtConfigFile configFile; - public State(String stepName, Map versions, File buildDir, NpmPathResolver npmPathResolver, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) throws IOException { + 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)), - "typescript-formatter", - NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-serve.js"), + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, + "/com/diffplug/spotless/npm/common-serve.js", + "/com/diffplug/spotless/npm/tsfmt-serve.js"), npmPathResolver.resolveNpmrcContent()), - buildDir, - npmPathResolver.resolveNpmExecutable()); + new NpmFormatterStepLocations( + projectDir, + buildDir, + cacheDir, + npmPathResolver)); this.buildDir = requireNonNull(buildDir); this.configFile = configFile; this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings); @@ -89,7 +94,7 @@ public State(String stepName, Map versions, File buildDir, NpmPa public FormatterFunc createFormatterFunc() { try { Map tsFmtOptions = unifyOptions(); - ServerProcessInfo tsfmtRestServer = npmRunServer(); + ServerProcessInfo tsfmtRestServer = toRuntime().npmRunServer(); TsFmtRestService restService = new TsFmtRestService(tsfmtRestServer.getBaseUrl()); return Closeable.ofDangerous(() -> endServer(restService, tsfmtRestServer), input -> restService.format(input, tsFmtOptions)); } catch (IOException e) { @@ -101,7 +106,7 @@ private Map unifyOptions() { Map unified = new HashMap<>(); if (!this.inlineTsFmtSettings.isEmpty()) { File targetFile = new File(this.buildDir, "inline-tsfmt.json"); - SimpleJsonWriter.of(this.inlineTsFmtSettings).toJsonFile(targetFile); + JsonWriter.of(this.inlineTsFmtSettings).toJsonFile(targetFile); unified.put("tsfmt", true); unified.put("tsfmtFile", targetFile.getAbsolutePath()); } else if (this.configFile != null) { @@ -115,7 +120,7 @@ private void endServer(TsFmtRestService restService, ServerProcessInfo restServe try { restService.shutdown(); } catch (Throwable t) { - logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t); + logger.info("Failed to request shutdown of rest service via api. Trying via process.", t); } restServer.close(); } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java index a47b608c36..61a53b4637 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,26 +18,19 @@ import java.util.LinkedHashMap; import java.util.Map; -public class TsFmtRestService { - - private final SimpleRestClient restClient; +public class TsFmtRestService extends BaseNpmRestService { TsFmtRestService(String baseUrl) { - this.restClient = SimpleRestClient.forBaseUrl(baseUrl); + super(baseUrl); } public String format(String fileContent, Map configOptions) { Map jsonProperties = new LinkedHashMap<>(); jsonProperties.put("file_content", fileContent); if (configOptions != null && !configOptions.isEmpty()) { - jsonProperties.put("config_options", SimpleJsonWriter.of(configOptions).toJsonRawValue()); + jsonProperties.put("config_options", JsonWriter.of(configOptions).toJsonRawValue()); } return restClient.postJson("/tsfmt/format", jsonProperties); } - - public String shutdown() { - return restClient.post("/shutdown"); - } - } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java b/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java index 75257711f6..c9b80394a8 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,9 @@ import static java.util.Objects.requireNonNull; import java.io.File; -import java.io.IOException; import java.io.Serializable; import java.util.Locale; -import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.ThrowingEx; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - public class TypedTsFmtConfigFile implements Serializable { private static final long serialVersionUID = -4442310349275775501L; @@ -35,18 +29,9 @@ public class TypedTsFmtConfigFile implements Serializable { private final File configFile; - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - @SuppressWarnings("unused") - private final FileSignature configFileSignature; - public TypedTsFmtConfigFile(TsConfigFileType configFileType, File configFile) { this.configFileType = requireNonNull(configFileType); this.configFile = requireNonNull(configFile); - try { - this.configFileSignature = FileSignature.signAsList(configFile); - } catch (IOException e) { - throw ThrowingEx.asRuntime(e); - } } TsConfigFileType configFileType() { 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 93c11e1e31..08a0308178 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 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ public class SortPomCfg implements Serializable { private static final long serialVersionUID = 1L; + public String version = "4.0.0"; + public String encoding = "UTF-8"; public String lineSeparator = System.getProperty("line.separator"); @@ -31,18 +33,26 @@ public class SortPomCfg implements Serializable { public boolean keepBlankLines = true; + public boolean endWithNewline = true; + public int nrOfIndentSpace = 2; public boolean indentBlankLines = false; public boolean indentSchemaLocation = false; + public String indentAttribute = null; + public String predefinedSortOrder = "recommended_2008_06"; + public boolean quiet = false; + public String sortOrderFile = null; public String sortDependencies = null; + public String sortDependencyManagement = null; + public String sortDependencyExclusions = null; public String sortPlugins = null; diff --git a/lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java b/lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java index 69d833f8e7..b082c021c4 100644 --- a/lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java +++ b/lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package com.diffplug.spotless.pom; -import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -25,24 +24,39 @@ import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; -public class SortPomStep { +public class SortPomStep implements Serializable { + private static final long serialVersionUID = 1L; + private static final String MAVEN_COORDINATE = "com.github.ekryd.sortpom:sortpom-sorter:"; public static final String NAME = "sortPom"; - private SortPomStep() {} + private final JarState.Promised jarState; + private final SortPomCfg cfg; + + private SortPomStep(JarState.Promised jarState, SortPomCfg cfg) { + this.jarState = jarState; + this.cfg = cfg; + } public static FormatterStep create(SortPomCfg cfg, Provisioner provisioner) { - return FormatterStep.createLazy(NAME, () -> new State(cfg, provisioner), State::createFormat); + return FormatterStep.create(NAME, + new SortPomStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + cfg.version, provisioner)), cfg), + SortPomStep::equalityState, + State::createFormat); + } + + private State equalityState() { + return new State(jarState.get(), cfg); } - static class State implements Serializable { + private static class State implements Serializable { private static final long serialVersionUID = 1; - SortPomCfg cfg; - JarState jarState; + private final SortPomCfg cfg; + private final JarState jarState; - public State(SortPomCfg cfg, Provisioner provisioner) throws IOException { + State(JarState jarState, SortPomCfg cfg) { + this.jarState = jarState; this.cfg = cfg; - this.jarState = JarState.from("com.github.ekryd.sortpom:sortpom-sorter:3.0.0", provisioner); } FormatterFunc createFormat() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { diff --git a/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java b/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java new file mode 100644 index 0000000000..e623bd0b51 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java @@ -0,0 +1,117 @@ +/* + * Copyright 2022-2024 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.protobuf; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class BufStep { + public static String name() { + return "buf"; + } + + public static String defaultVersion() { + return "1.44.0"; + } + + private final String version; + private final @Nullable String pathToExe; + + private BufStep(String version, @Nullable String pathToExe) { + this.version = version; + this.pathToExe = pathToExe; + } + + public static BufStep withVersion(String version) { + return new BufStep(version, null); + } + + public BufStep withPathToExe(String pathToExe) { + return new BufStep(version, pathToExe); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createRoundtrip, RoundtripState::state, State::toFunc); + } + + private RoundtripState createRoundtrip() { + String instructions = "https://docs.buf.build/installation"; + ForeignExe exe = ForeignExe.nameAndVersion("buf", version) + .pathToExe(pathToExe) + .versionRegex(Pattern.compile("(\\S*)")) + .fixCantFind("Try following the instructions at " + instructions + ", or else tell Spotless where it is with {@code buf().pathToExe('path/to/executable')}"); + return new RoundtripState(version, exe); + } + + private static class RoundtripState implements Serializable { + private static final long serialVersionUID = 1L; + + final String version; + final ForeignExe exe; + + RoundtripState(String version, ForeignExe exe) { + this.version = version; + this.exe = exe; + } + + private State state() { + return new State(version, exe); + } + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + static class State implements Serializable { + private static final long serialVersionUID = -1825662356883926318L; + // used for up-to-date checks and caching + final String version; + + // used for executing + private final transient ForeignExe exe; + private transient String exeAbsPath; + + State(String version, ForeignExe exeAbsPath) { + this.version = version; + this.exe = Objects.requireNonNull(exeAbsPath); + } + + String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + if (exeAbsPath == null) { + exeAbsPath = exe.confirmVersionAndGetAbsolutePath(); + } + List args = List.of(exeAbsPath, "format", file.getAbsolutePath()); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/package-info.java b/lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java similarity index 73% rename from _ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/package-info.java rename to lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java index 5ed08616b9..cd91e2671c 100644 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/runtime/package-info.java +++ b/lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2022-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** - * Eclipse Core Runtime adapters allowing runtime modifications which normally requires an Eclipse restart. - */ -package com.diffplug.spotless.extra.eclipse.base.runtime; +package com.diffplug.spotless.protobuf; + +public class ProtobufConstants { + public static final String LICENSE_HEADER_DELIMITER = "syntax"; +} diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index c19d023ffc..46d00caab2 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,8 @@ import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -36,7 +36,7 @@ public static String name() { } public static String defaultVersion() { - return "19.10b0"; + return "22.3.0"; } private final String version; @@ -56,33 +56,53 @@ public BlackStep withPathToExe(String pathToExe) { } public FormatterStep create() { - return FormatterStep.createLazy(name(), this::createState, State::toFunc); + return FormatterStep.createLazy(name(), this::createRoundtrip, RoundtripState::toEquality, EqualityState::toFunc); } - private State createState() throws IOException, InterruptedException { + private RoundtripState createRoundtrip() { String trackingIssue = "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/674"; - String exeAbsPath = ForeignExe.nameAndVersion("black", version) + ForeignExe exeAbsPath = ForeignExe.nameAndVersion("black", version) .pathToExe(pathToExe) + .versionRegex(Pattern.compile("(?:black, version|black,|version) (\\S*)")) .fixCantFind("Try running {@code pip install black=={version}}, or else tell Spotless where it is with {@code black().pathToExe('path/to/executable')}" + trackingIssue) - .fixWrongVersion("Try running {@code pip install --force-reinstall black=={version}}, or else specify {@code black('{versionFound}')} to Spotless" + trackingIssue) - .confirmVersionAndGetAbsolutePath(); - return new State(this, exeAbsPath); + .fixWrongVersion("Try running {@code pip install --force-reinstall black=={version}}, or else specify {@code black('{versionFound}')} to Spotless" + trackingIssue); + return new RoundtripState(version, exeAbsPath); + } + + static class RoundtripState implements Serializable { + private static final long serialVersionUID = 1L; + + final String version; + final ForeignExe exe; + + RoundtripState(String version, ForeignExe exe) { + this.version = version; + this.exe = exe; + } + + private EqualityState toEquality() { + return new EqualityState(version, exe); + } } @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") - static class State implements Serializable { + static class EqualityState implements Serializable { private static final long serialVersionUID = -1825662356883926318L; // used for up-to-date checks and caching final String version; + final transient ForeignExe exe; // used for executing - final transient List args; + private transient @Nullable String[] args; - State(BlackStep step, String exeAbsPath) { - this.version = step.version; - this.args = Arrays.asList(exeAbsPath, "-"); + EqualityState(String version, ForeignExe exeAbsPath) { + this.version = version; + this.exe = Objects.requireNonNull(exeAbsPath); } String format(ProcessRunner runner, String input) throws IOException, InterruptedException { + if (args == null) { + args = new String[]{exe.confirmVersionAndGetAbsolutePath(), "-"}; + } return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.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 new file mode 100644 index 0000000000..13c5bfcaa4 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterConfig.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024 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.rdf; + +import java.io.Serializable; +import java.util.Objects; + +public class RdfFormatterConfig implements Serializable { + private static final long serialVersionUID = 1L; + private boolean failOnWarning = true; + private String turtleFormatterVersion = RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION; + private boolean verify = true; + + public RdfFormatterConfig() {} + + public void setFailOnWarning(boolean failOnWarning) { + this.failOnWarning = failOnWarning; + } + + public boolean isFailOnWarning() { + return failOnWarning; + } + + public boolean isVerify() { + return verify; + } + + public void setVerify(boolean verify) { + this.verify = verify; + } + + public static Builder builder() { + return new Builder(); + } + + public String getTurtleFormatterVersion() { + return turtleFormatterVersion; + } + + public void setTurtleFormatterVersion(String turtleFormatterVersion) { + this.turtleFormatterVersion = turtleFormatterVersion; + } + + public static class Builder { + RdfFormatterConfig config = new RdfFormatterConfig(); + + public Builder() {} + + public Builder failOnWarning() { + return this.failOnWarning(true); + } + + public Builder failOnWarning(boolean fail) { + this.config.setFailOnWarning(fail); + return this; + } + + public Builder turtleFormatterVersion(String version) { + this.config.turtleFormatterVersion = version; + return this; + } + + public Builder verify(boolean verify) { + this.config.verify = verify; + return this; + } + + public Builder verify() { + this.config.verify = true; + return this; + } + + public RdfFormatterConfig build() { + return config; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof RdfFormatterConfig)) + return false; + RdfFormatterConfig that = (RdfFormatterConfig) o; + return isFailOnWarning() == that.isFailOnWarning() + && Objects.equals(turtleFormatterVersion, that.turtleFormatterVersion); + } + + @Override + public int hashCode() { + return Objects.hash(isFailOnWarning(), turtleFormatterVersion); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterFunc.java new file mode 100644 index 0000000000..03665b69a2 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterFunc.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 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.rdf; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.LineEnding; + +public class RdfFormatterFunc implements FormatterFunc { + private static final Set TURTLE_EXTENSIONS = Set.of("ttl", "turtle"); + private static final Set TRIG_EXTENSIONS = Set.of("trig"); + private static final Set NTRIPLES_EXTENSIONS = Set.of("n-triples", "ntriples", "nt"); + private static final Set NQUADS_EXTENSIONS = Set.of("n-quads", "nquads", "nq"); + + private final RdfFormatterStep.State state; + private final ReflectionHelper reflectionHelper; + + public RdfFormatterFunc(RdfFormatterStep.State state) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + this.state = state; + this.reflectionHelper = new ReflectionHelper(state); + } + + @Override + public String apply(String input) throws Exception { + throw new UnsupportedOperationException("We need to know the filename so we can guess the RDF format. Use apply(String, File) instead!"); + } + + @Override + public String apply(String rawUnix, File file) throws Exception { + String filename = file.getName().toLowerCase(Locale.US); + int lastDot = filename.lastIndexOf('.'); + if (lastDot < 0) { + throw new IllegalArgumentException( + String.format("File %s has no file extension, cannot determine RDF format", file.getAbsolutePath())); + } + if (lastDot + 1 >= filename.length()) { + throw new IllegalArgumentException( + String.format("File %s has no file extension, cannot determine RDF format", file.getAbsolutePath())); + } + String extension = filename.substring(lastDot + 1); + + try { + if (TURTLE_EXTENSIONS.contains(extension)) { + return formatTurtle(rawUnix, file, reflectionHelper); + } + if (TRIG_EXTENSIONS.contains(extension)) { + return formatTrig(rawUnix, file); + } + if (NTRIPLES_EXTENSIONS.contains(extension)) { + return formatNTriples(rawUnix, file); + } + if (NQUADS_EXTENSIONS.contains(extension)) { + return formatNQuads(rawUnix, file); + } + throw new IllegalArgumentException(String.format("Cannot handle file with extension %s", extension)); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error formatting file " + file.getPath(), e.getCause()); + } catch (Exception e) { + throw new RuntimeException("Error formatting file " + file.getPath(), e); + } + } + + private String formatNQuads(String rawUnix, File file) { + throw new UnsupportedOperationException("NQUADS formatting not supported yet"); + } + + private String formatNTriples(String rawUnix, File file) { + throw new UnsupportedOperationException("NTRIPLES formatting not supported yet"); + } + + private String formatTrig(String rawUnix, File file) { + throw new UnsupportedOperationException("TRIG formatting not supported yet"); + } + + private String formatTurtle(String rawUnix, File file, ReflectionHelper reflectionHelper) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, + NoSuchFieldException, InstantiationException { + String formatted; + Object lang = reflectionHelper.getLang("TTL"); + formatted = reflectionHelper.formatWithTurtleFormatter(rawUnix); + if (state.getConfig().isVerify()) { + veryfyResult(rawUnix, file, reflectionHelper, lang, formatted); + } + return LineEnding.toUnix(formatted); + } + + private static void veryfyResult(String rawUnix, File file, ReflectionHelper reflectionHelper, Object lang, + String formatted) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Object modelBefore = reflectionHelper.parseToModel(rawUnix, file, lang); + Object modelAfter = reflectionHelper.parseToModel(formatted, file, lang); + if (!reflectionHelper.areModelsIsomorphic(modelBefore, modelAfter)) { + long beforeSize = reflectionHelper.modelSize(modelBefore); + long afterSize = reflectionHelper.modelSize(modelAfter); + String diffResult; + if (beforeSize != afterSize) { + diffResult = String.format("< %,d triples", beforeSize); + diffResult += String.format("> %,d triples", afterSize); + } else { + diffResult = calculateDiff(reflectionHelper, modelBefore, modelAfter); + } + throw new IllegalStateException( + "Formatted RDF is not isomorphic with original, which means that formatting changed the data.\n" + + "This could be a bug in the formatting system leading to data corruption and should be reported. \n" + + "If you are not scared to lose data, you can disable this check by setting the config option 'verify' to 'false'" + + "\n\nDiff:\n" + + diffResult); + } + } + + private static String calculateDiff(ReflectionHelper reflectionHelper, Object modelBefore, Object modelAfter) + throws InvocationTargetException, IllegalAccessException { + String diffResult; + Object graphBefore = reflectionHelper.getGraph(modelBefore); + Object graphAfter = reflectionHelper.getGraph(modelAfter); + + List onlyInBeforeContent = reflectionHelper.streamGraph(graphBefore) + .filter(triple -> { + try { + return !reflectionHelper.graphContainsSameTerm(graphAfter, triple); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + + List onlyInAfterContent = reflectionHelper.streamGraph(graphAfter) + .filter(triple -> { + try { + return !reflectionHelper.graphContainsSameTerm(graphBefore, triple); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + if (!(onlyInBeforeContent.isEmpty() && onlyInAfterContent.isEmpty())) { + diffResult = onlyInBeforeContent.stream().map(s -> String.format("< %s", s)) + .collect(Collectors.joining("\n")); + diffResult += "\n" + onlyInAfterContent.stream().map(s -> String.format("> %s", s)).collect(Collectors.joining("\n")); + } else { + diffResult = "'before' and 'after' content differs, but we don't know why. This is probably a bug."; + } + return diffResult; + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java new file mode 100644 index 0000000000..a5ed70bfed --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rdf/RdfFormatterStep.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024 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.rdf; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +public class RdfFormatterStep implements Serializable { + public static final String LATEST_TURTLE_FORMATTER_VERSION = "1.2.13"; + private static final long serialVersionUID = 1L; + + private static final String TURTLE_FORMATTER_COORDINATES = "de.atextor:turtle-formatter"; + + private final JarState.Promised jarState; + private final Map turtleFormatterStyle; + private final RdfFormatterConfig config; + + public static FormatterStep create(RdfFormatterConfig config, Map turtleOptions, Provisioner provisioner) + throws ClassNotFoundException { + JarState.Promised jarState; + jarState = JarState.promise(() -> JarState.from(TURTLE_FORMATTER_COORDINATES + ":" + config.getTurtleFormatterVersion(), provisioner)); + RdfFormatterStep step = new RdfFormatterStep(jarState, config, turtleOptions); + return FormatterStep.create("RdfFormatter", step, RdfFormatterStep::state, RdfFormatterStep::formatterFunc); + } + + public static State state(RdfFormatterStep step) { + return new State(step.config, step.turtleFormatterStyle, step.jarState.get()); + } + + public static RdfFormatterFunc formatterFunc(State state) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return new RdfFormatterFunc(state); + } + + public RdfFormatterStep(JarState.Promised jarState, RdfFormatterConfig config, + Map turtleFormatterStyle) { + this.jarState = jarState; + this.turtleFormatterStyle = turtleFormatterStyle; + this.config = config; + } + + public static class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final RdfFormatterConfig config; + + private final Map turtleFormatterStyle; + + private final JarState jarState; + + public State(RdfFormatterConfig config, Map turtleFormatterStyle, + JarState jarState) { + this.config = config; + this.turtleFormatterStyle = new TreeMap<>(turtleFormatterStyle == null ? Map.of() : turtleFormatterStyle); + this.jarState = jarState; + } + + public RdfFormatterConfig getConfig() { + return config; + } + + public Map getTurtleFormatterStyle() { + return turtleFormatterStyle; + } + + public JarState getJarState() { + return jarState; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof State)) + return false; + State state = (State) o; + return Objects.equals(getConfig(), state.getConfig()) && Objects.equals( + getTurtleFormatterStyle(), state.getTurtleFormatterStyle()) + && Objects.equals( + getJarState(), state.getJarState()); + } + + @Override + public int hashCode() { + return Objects.hash(getConfig(), getTurtleFormatterStyle(), getJarState()); + } + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java b/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java new file mode 100644 index 0000000000..435e9757af --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rdf/ReflectionHelper.java @@ -0,0 +1,548 @@ +/* + * Copyright 2024 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.rdf; + +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; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.net.URI; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ReflectionHelper { + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final RdfFormatterStep.State state; + private final ClassLoader classLoader; + private final Class JenaRdfDataMgrClass; + private final Class JenaRdfParserClass; + private final Class JenaRdfParserBuilderClass; + private final Class JenaErrorHandlerClass; + private final Class JenaModelClass; + private final Class JenaStmtIteratorClass; + private final Class JenaStatementClass; + private final Class JenaRDFNodeClass; + private final Class JenaResourceClass; + private final Class JenaPropertyClass; + private final Class JenaModelFactoryClass; + private final Class JenaLangClass; + private final Class JenaRDFFormatClass; + private final Class JenaGraphClass; + private final Class JenaTriple; + private final Class TurtleFormatFormattingStyleClass; + private final Class TurtleFormatFormattingStyleBuilderClass; + private final Class TurtleFormatFormatterClass; + private final Class TurtleFormatKnownPrefix; + + private final Method graphStream; + private final Method graphFindTriple; + private final Method contains; + private final Method getSubject; + private final Method getPredicate; + private final Method getObject; + private final Method isAnon; + private final Method getGraph; + private final Method tripleGetObject; + + private Object turtleFormatter = null; + private Object jenaModelInstance = null; + + public ReflectionHelper(RdfFormatterStep.State state) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + this.state = state; + this.classLoader = state.getJarState().getClassLoader(); + this.JenaRdfDataMgrClass = classLoader.loadClass("org.apache.jena.riot.RDFDataMgr"); + this.JenaRdfParserClass = classLoader.loadClass("org.apache.jena.riot.RDFParser"); + this.JenaRdfParserBuilderClass = classLoader.loadClass("org.apache.jena.riot.RDFParserBuilder"); + this.JenaErrorHandlerClass = classLoader.loadClass("org.apache.jena.riot.system.ErrorHandler"); + this.JenaModelClass = classLoader.loadClass("org.apache.jena.rdf.model.Model"); + this.JenaStmtIteratorClass = classLoader.loadClass("org.apache.jena.rdf.model.StmtIterator"); + this.JenaRDFNodeClass = classLoader.loadClass("org.apache.jena.rdf.model.RDFNode"); + this.JenaResourceClass = classLoader.loadClass("org.apache.jena.rdf.model.Resource"); + this.JenaPropertyClass = classLoader.loadClass("org.apache.jena.rdf.model.Property"); + this.JenaStatementClass = classLoader.loadClass("org.apache.jena.rdf.model.Statement"); + 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 -> c.getSimpleName().equals("FormattingStyleBuilder")).findFirst().get(); + this.TurtleFormatKnownPrefix = Arrays.stream(innerClasses).filter(c -> c.getSimpleName().equals("KnownPrefix")).findFirst().get(); + this.getSubject = JenaStatementClass.getMethod("getSubject"); + this.getPredicate = JenaStatementClass.getMethod("getPredicate"); + this.getObject = JenaStatementClass.getMethod("getObject"); + this.isAnon = JenaRDFNodeClass.getMethod("isAnon"); + this.getGraph = JenaModelClass.getMethod(("getGraph")); + this.JenaGraphClass = classLoader.loadClass("org.apache.jena.graph.Graph"); + this.JenaTriple = classLoader.loadClass("org.apache.jena.graph.Triple"); + this.graphFindTriple = JenaGraphClass.getMethod("find", JenaTriple); + this.graphStream = JenaGraphClass.getMethod("stream"); + this.tripleGetObject = JenaTriple.getMethod("getObject"); + this.contains = JenaGraphClass.getMethod("contains", JenaTriple); + this.jenaModelInstance = JenaModelFactoryClass.getMethod("createDefaultModel").invoke(JenaModelFactoryClass); + } + + public Object getLang(String lang) throws NoSuchFieldException, IllegalAccessException { + return this.JenaLangClass.getDeclaredField(lang).get(this.JenaLangClass); + } + + public Object getModel() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return JenaModelFactoryClass.getMethod("createDefaultModel").invoke(JenaModelFactoryClass); + } + + public Object getErrorHandler(File file) { + return Proxy.newProxyInstance(this.classLoader, + new Class[]{JenaErrorHandlerClass}, new DynamicErrorInvocationHandler(file)); + } + + public Object listModelStatements(Object modelBefore) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method listStatements = JenaModelClass.getMethod("listStatements"); + return listStatements.invoke(modelBefore); + } + + public boolean hasNext(Object statementIterator) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method hasNext = JenaStmtIteratorClass.getMethod("hasNext"); + return (boolean) hasNext.invoke(statementIterator); + } + + public Object next(Object statementIterator) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method hasNext = JenaStmtIteratorClass.getMethod("next"); + return hasNext.invoke(statementIterator); + } + + public boolean containsBlankNode(Object statement) + throws InvocationTargetException, IllegalAccessException { + Object subject = getSubject.invoke(statement); + if ((boolean) isAnon.invoke(subject)) { + return true; + } + Object predicate = getPredicate.invoke(statement); + if ((boolean) isAnon.invoke(predicate)) { + return true; + } + Object object = getObject.invoke(statement); + return (boolean) isAnon.invoke(object); + } + + public boolean containsStatement(Object model, Object statement) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method contains = JenaModelClass.getMethod("contains", JenaStatementClass); + return (boolean) contains.invoke(model, statement); + } + + public boolean graphContainsSameTerm(Object graph, Object triple) throws InvocationTargetException, IllegalAccessException { + boolean found = (boolean) contains.invoke(graph, triple); + if (!found) { + return false; + } + Iterator it = (Iterator) graphFindTriple.invoke(graph, triple); + while (it.hasNext()) { + Object foundTriple = it.next(); + Object foundObject = tripleGetObject.invoke(foundTriple); + Object searchedObject = tripleGetObject.invoke(triple); + if (!foundObject.equals(searchedObject)) { + return false; + } + } + return true; + } + + private class DynamicErrorInvocationHandler implements InvocationHandler { + private final String filePath; + + public DynamicErrorInvocationHandler(File file) { + if (state.getConfig().isFailOnWarning()) { + this.filePath = null; + } else { + this.filePath = file.getPath(); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String message = (String) args[0]; + long line = (long) args[1]; + long col = (long) args[2]; + String severity = method.getName(); + if (severity.equals("warning") && !state.getConfig().isFailOnWarning()) { + logger.warn("{}({},{}): {}", this.filePath, line, col, message); + } else { + if (severity.equals("warning")) { + logger.error("Formatter fails because of a parser warning. To make the formatter succeed in" + + "the presence of warnings, set the configuration parameter 'failOnWarning' to 'false' (default: 'true')"); + } + throw new RuntimeException( + String.format("line %d, col %d: %s (severity: %s)", line, col, message, severity)); + } + return null; + } + } + + public Object getParser(Object lang, Object errorHandler, String rawUnix) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Object parserBuilder = JenaRdfParserClass.getMethod("create").invoke(JenaRdfParserClass); + parserBuilder = JenaRdfParserBuilderClass.getMethod("errorHandler", JenaErrorHandlerClass) + .invoke(parserBuilder, errorHandler); + parserBuilder = JenaRdfParserBuilderClass.getMethod("forceLang", JenaLangClass).invoke(parserBuilder, lang); + parserBuilder = JenaRdfParserBuilderClass.getMethod("strict", Boolean.TYPE).invoke(parserBuilder, true); + parserBuilder = JenaRdfParserBuilderClass.getMethod("checking", Boolean.TYPE).invoke(parserBuilder, true); + parserBuilder = JenaRdfParserBuilderClass.getMethod("fromString", String.class).invoke(parserBuilder, rawUnix); + return JenaRdfParserBuilderClass.getMethod("build").invoke(parserBuilder); + } + + public void parseModel(Object parser, Object model) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + JenaRdfParserClass.getMethod("parse", JenaModelClass).invoke(parser, model); + } + + public Object getGraph(Object model) throws InvocationTargetException, IllegalAccessException { + return getGraph.invoke(model); + } + + public Stream streamGraph(Object graph) throws InvocationTargetException, IllegalAccessException { + return (Stream) graphStream.invoke(graph); + + } + + public String formatWithJena(Object model, Object rdfFormat) + throws NoSuchMethodException, NoSuchFieldException, InvocationTargetException, IllegalAccessException { + StringWriter sw = new StringWriter(); + JenaRdfDataMgrClass + .getMethod("write", StringWriter.class, JenaModelClass, JenaRDFFormatClass) + .invoke(JenaRdfDataMgrClass, sw, model, rdfFormat); + return sw.toString(); + } + + public String formatWithTurtleFormatter(Object model) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + Object formatter = getTurtleFormatter(); + return (String) TurtleFormatFormatterClass.getMethod("apply", JenaModelClass).invoke(formatter, model); + } + + private synchronized Object getTurtleFormatter() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { + if (this.turtleFormatter == null) { + Object style = newTurtleFormatterStyle(); + this.turtleFormatter = newTurtleFormatter(style); + } + return this.turtleFormatter; + } + + private Object newTurtleFormatterStyle() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + Object builder = TurtleFormatFormattingStyleClass.getMethod("builder").invoke(TurtleFormatFormatterClass); + for (String optionName : state.getTurtleFormatterStyle().keySet()) { + Method method = getBuilderMethod(optionName); + callBuilderMethod(builder, method, state.getTurtleFormatterStyle().get(optionName)); + } + Object style = TurtleFormatFormattingStyleBuilderClass.getMethod("build").invoke(builder); + return style; + } + + public String formatWithTurtleFormatter(String ttlContent) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { + Object style = newTurtleFormatterStyle(); + Object formatter = newTurtleFormatter(style); + return (String) TurtleFormatFormatterClass.getMethod("applyToContent", String.class).invoke(formatter, ttlContent); + } + + private Object newTurtleFormatter(Object style) + throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + Object formatter = TurtleFormatFormatterClass.getConstructor(TurtleFormatFormattingStyleClass) + .newInstance(style); + return formatter; + } + + private void callBuilderMethod(Object builder, Method method, String parameterValueAsString) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Class param = method.getParameterTypes()[0]; + if (param.isEnum()) { + List selectedEnumValueList = Arrays.stream(param.getEnumConstants()).filter(e -> { + try { + return e.getClass().getMethod("name").invoke(e).equals(parameterValueAsString); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { + throw new RuntimeException(ex); + } + }).collect( + Collectors.toList()); + if (selectedEnumValueList.isEmpty()) { + throw new IllegalArgumentException( + String.format("Cannot set config option %s to value %s: value must be one of %s", + method.getName(), + parameterValueAsString, + Arrays.stream(param.getEnumConstants()).map(e -> { + try { + return (String) e.getClass().getMethod("name").invoke(e); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { + throw new RuntimeException(ex); + } + }).collect( + Collectors.joining(",", "[", "]")))); + } else if (selectedEnumValueList.size() > 1) { + throw new IllegalArgumentException( + String.format("Found more than 1 enum value for name %s, that should never happen", + parameterValueAsString)); + } + method.invoke(builder, selectedEnumValueList.get(0)); + } else if (param.equals(NumberFormat.class)) { + method.invoke(builder, new DecimalFormat(parameterValueAsString, DecimalFormatSymbols.getInstance(Locale.US))); + } else if (param.equals(Boolean.class) || param.equals(Boolean.TYPE)) { + method.invoke(builder, Boolean.parseBoolean(parameterValueAsString)); + } else if (param.equals(String.class)) { + method.invoke(builder, parameterValueAsString); + } else if (param.equals(Integer.class)) { + method.invoke(builder, Integer.parseInt(parameterValueAsString)); + } else if (param.equals(Double.class)) { + method.invoke(builder, Double.parseDouble(parameterValueAsString)); + } else if (param.equals(Long.class)) { + method.invoke(builder, Long.parseLong(parameterValueAsString)); + } else if (param.equals(Float.class)) { + method.invoke(builder, Float.parseFloat(parameterValueAsString)); + } else if (Set.class.isAssignableFrom(param)) { + method.invoke(builder, makeSetOf(((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0], parameterValueAsString)); + } else if (List.class.isAssignableFrom(param)) { + method.invoke(builder, makeListOf(((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0], parameterValueAsString)); + } else { + throw new IllegalArgumentException(String.format( + "Cannot handle turtle-formatter config option %s: parameters of type %s are not implemented in the spotless plugin yet", + method.getName(), param.getName())); + } + } + + private Object makeListOf(Type type, String parameterValueAsString) { + String[] entries = split(parameterValueAsString); + List ret = Arrays.stream(entries).map(e -> { + try { + return instantiate(type, e); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + }).collect(Collectors.toList()); + return ret; + } + + private Object makeSetOf(Type type, String parameterValueAsString) { + String[] entries = split(parameterValueAsString); + return Arrays.stream(entries).map(e -> { + try { + return instantiate(type, e); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + }).collect(Collectors.toSet()); + } + + private Object instantiate(Type type, String stringRepresentation) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + if (type.equals(String.class)) { + return stringRepresentation; + } + if (type.equals(JenaRDFNodeClass)) { + try { + String uri = tryToMakeUri(stringRepresentation); + return this.JenaModelClass.getMethod("createResource", String.class) + .invoke(this.jenaModelInstance, uri); + } catch (IllegalArgumentException e) { + return this.JenaModelClass.getMethod("createLiteral", String.class, String.class) + .invoke(this.jenaModelInstance, stringRepresentation, ""); + } + } + if (type.equals(JenaResourceClass)) { + return this.JenaModelClass.getMethod("createResource", String.class).invoke(this.jenaModelInstance, tryToMakeUri(stringRepresentation)); + } + if (type.equals(JenaPropertyClass)) { + String uri = tryToMakeUri(stringRepresentation); + if (uri != null) { + String localname = uri.replaceAll("^.+[#/]", ""); + String namespace = uri.substring(0, uri.length() - localname.length()); + return this.JenaModelClass.getMethod("createProperty", String.class, String.class) + .invoke(this.jenaModelInstance, namespace, localname); + } + } + if (type.equals(TurtleFormatKnownPrefix)) { + return getKnownPrefix(stringRepresentation); + } + throw new IllegalArgumentException(String.format("Cannot instantiate class %s from string representation %s", type, stringRepresentation)); + } + + private String tryToMakeUri(String stringRepresentation) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + if (stringRepresentation.matches("[^:/]+:[^:/]+")) { + int colonIndex = stringRepresentation.indexOf(':'); + //could be a known prefix + String prefix = stringRepresentation.substring(0, colonIndex); + Object knownPrefix = getKnownPrefix(prefix); + String base = this.TurtleFormatKnownPrefix.getMethod("iri").invoke(knownPrefix).toString(); + return base + stringRepresentation.substring(colonIndex + 1); + } + // try to parse a URI - throws an IllegalArgumentException if it is not a URI + URI uri = URI.create(stringRepresentation); + return uri.toString(); + } + + 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; + } + } + } + throw new IllegalArgumentException(String.format("Unable to find FormattingStyle.KnownPrefix for prefix '%s'. Options are: %s", stringRepresentation, options.stream().collect( + Collectors.joining(",\n\t", "\n\t", "\n")))); + } + + private static String[] split(String parameterValueAsString) { + if (parameterValueAsString == null || parameterValueAsString.isBlank()) { + return new String[0]; + } + return parameterValueAsString.split("\\s*(,|,\\s*\n|\n)\\s*"); + } + + private Method getBuilderMethod(String optionName) { + Method[] allMethods = TurtleFormatFormattingStyleBuilderClass.getDeclaredMethods(); + List methods = Arrays.stream(allMethods).filter(m -> m.getName().equals(optionName)) + .collect( + Collectors.toList()); + if (methods.isEmpty()) { + List candidates = Arrays.stream(allMethods).filter(m -> m.getParameterCount() == 1) + .sorted(Comparator.comparing(Method::getName)).collect( + Collectors.toList()); + throw new RuntimeException( + String.format("Unrecognized configuration parameter name: %s. Candidates are:%n%s", optionName, candidates.stream().map(Method::getName).collect( + Collectors.joining("\n\t", "\t", "")))); + } + if (methods.size() > 1) { + throw new RuntimeException( + String.format("More than one builder method found for configuration parameter name: %s", + optionName)); + } + Method method = methods.get(0); + if (method.getParameterCount() != 1) { + throw new RuntimeException( + String.format("Method with unexpected parameter count %s found for configuration parameter name: %s", + method.getParameterCount(), + optionName)); + } + return method; + } + + public Object getRDFFormat(String rdfFormat) throws NoSuchFieldException, IllegalAccessException { + return JenaRDFFormatClass.getDeclaredField(rdfFormat).get(JenaRDFFormatClass); + } + + public Object parseToModel(String rawUnix, File file, Object lang) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Object model = getModel(); + Object errorHandler = getErrorHandler(file); + Object parser = getParser(lang, errorHandler, rawUnix); + parseModel(parser, model); + return model; + } + + public boolean areModelsIsomorphic(Object leftModel, Object rightModel) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method isIsomorphicWith = JenaModelClass.getMethod("isIsomorphicWith", JenaModelClass); + return (boolean) isIsomorphicWith.invoke(leftModel, rightModel); + } + + public long modelSize(Object model) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method size = JenaModelClass.getMethod("size"); + return (long) size.invoke(model); + } + + private static class SortedModelInvocationHandler implements InvocationHandler { + private final ReflectionHelper reflectionHelper; + private final Object jenaModel; + + public SortedModelInvocationHandler(ReflectionHelper reflectionHelper, Object jenaModel) { + this.reflectionHelper = reflectionHelper; + this.jenaModel = jenaModel; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("listSubjects") && method.getParameterCount() == 0) { + Object resIterator = method.invoke(jenaModel); + List resources = new ArrayList<>(); + while (hasNext(resIterator)) { + resources.add(next(resIterator)); + } + resources.sort(Comparator.comparing(x -> { + try { + return (String) x.getClass().getMethod("getURI").invoke(x); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }).thenComparing(x -> { + Object anonId; + try { + anonId = x.getClass().getMethod("getAnonId").invoke(x); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + if (anonId != null) { + return anonId.toString(); + } + return null; + })); + return reflectionHelper.classLoader.loadClass("org.apache.jena.rdf.model.impl.ResIteratorImpl") + .getConstructor( + Iterator.class, Object.class) + .newInstance(resources.iterator(), null); + } + return method.invoke(jenaModel); + } + + boolean hasNext(Object it) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return (boolean) it.getClass().getMethod("hasNext").invoke(it); + } + + Object next(Object it) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return it.getClass().getMethod("next").invoke(it); + } + + } +} 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 8c8e4ccb2d..9a2ec248ee 100644 --- a/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,8 @@ import java.io.File; import java.io.IOException; import java.io.Serializable; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.lang.reflect.Constructor; import java.util.Collections; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -35,27 +30,38 @@ import com.diffplug.spotless.Provisioner; /** Wraps up scalafmt as a FormatterStep. */ -public class ScalaFmtStep { - // prevent direct instantiation - private ScalaFmtStep() {} - - private static final Pattern VERSION_PRE_2_0 = Pattern.compile("[10]\\.(\\d+)\\.\\d+"); - private static final Pattern VERSION_PRE_3_0 = Pattern.compile("2\\.(\\d+)\\.\\d+"); - private static final String DEFAULT_VERSION = "3.0.8"; - static final String NAME = "scalafmt"; - static final String MAVEN_COORDINATE_PRE_2_0 = "com.geirsson:scalafmt-core_2.11:"; - static final String MAVEN_COORDINATE_PRE_3_0 = "org.scalameta:scalafmt-core_2.11:"; - static final String MAVEN_COORDINATE = "org.scalameta:scalafmt-core_2.13:"; +public class ScalaFmtStep implements Serializable { + private static final long serialVersionUID = 1L; + + static final String DEFAULT_VERSION = "3.8.1"; + + private static final String DEFAULT_SCALA_MAJOR_VERSION = "2.13"; + private static final String NAME = "scalafmt"; + private static final String MAVEN_COORDINATE = "org.scalameta:scalafmt-core_"; + + private final JarState.Promised jarState; + @Nullable + private final File configFile; + + private ScalaFmtStep(JarState.Promised jarState, @Nullable File configFile) { + this.jarState = jarState; + this.configFile = configFile; + } public static FormatterStep create(Provisioner provisioner) { - return create(defaultVersion(), provisioner, null); + return create(defaultVersion(), defaultScalaMajorVersion(), provisioner, null); } public static FormatterStep create(String version, Provisioner provisioner, @Nullable File configFile) { - Objects.requireNonNull(version, "version"); - Objects.requireNonNull(provisioner, "provisioner"); - return FormatterStep.createLazy(NAME, - () -> new State(version, provisioner, configFile), + return create(version, defaultScalaMajorVersion(), provisioner, configFile); + } + + public static FormatterStep create(String version, @Nullable String scalaMajorVersion, Provisioner provisioner, @Nullable File configFile) { + String finalScalaMajorVersion = scalaMajorVersion == null ? DEFAULT_SCALA_MAJOR_VERSION : scalaMajorVersion; + + return FormatterStep.create(NAME, + new ScalaFmtStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + finalScalaMajorVersion + ":" + version, provisioner)), configFile), + ScalaFmtStep::equalityState, State::createFormat); } @@ -63,84 +69,30 @@ public static String defaultVersion() { return DEFAULT_VERSION; } - static final class State implements Serializable { + public static String defaultScalaMajorVersion() { + return DEFAULT_SCALA_MAJOR_VERSION; + } + + private State equalityState() throws IOException { + return new State(jarState.get(), configFile); + } + + private static final class State implements Serializable { private static final long serialVersionUID = 1L; - final JarState jarState; - final FileSignature configSignature; - - State(String version, Provisioner provisioner, @Nullable File configFile) throws IOException { - String mavenCoordinate; - Matcher versionMatcher; - if ((versionMatcher = VERSION_PRE_2_0.matcher(version)).matches()) { - mavenCoordinate = MAVEN_COORDINATE_PRE_2_0; - } else if ((versionMatcher = VERSION_PRE_3_0.matcher(version)).matches()) { - mavenCoordinate = MAVEN_COORDINATE_PRE_3_0; - } else { - mavenCoordinate = MAVEN_COORDINATE; - } - - this.jarState = JarState.from(mavenCoordinate + version, provisioner); + private final JarState jarState; + private final FileSignature configSignature; + + State(JarState jarState, @Nullable File configFile) throws IOException { + this.jarState = jarState; this.configSignature = FileSignature.signAsList(configFile == null ? Collections.emptySet() : Collections.singleton(configFile)); } FormatterFunc createFormat() throws Exception { - ClassLoader classLoader = jarState.getClassLoader(); - - // scalafmt returns instances of formatted, we get result by calling get() - Class formatted = classLoader.loadClass("org.scalafmt.Formatted"); - Method formattedGet = formatted.getMethod("get"); - - // this is how we actually do a format - Class scalafmt = classLoader.loadClass("org.scalafmt.Scalafmt"); - Class scalaSet = classLoader.loadClass("scala.collection.immutable.Set"); - - Object defaultScalaFmtConfig = scalafmt.getMethod("format$default$2").invoke(null); - Object emptyRange = scalafmt.getMethod("format$default$3").invoke(null); - Method formatMethod = scalafmt.getMethod("format", String.class, defaultScalaFmtConfig.getClass(), scalaSet); - - // now we just need to parse the config, if any - Object config; - if (configSignature.files().isEmpty()) { - config = defaultScalaFmtConfig; - } else { - File file = configSignature.getOnlyFile(); - - Class optionCls = classLoader.loadClass("scala.Option"); - Class configCls = classLoader.loadClass("org.scalafmt.config.Config"); - Class scalafmtCls = classLoader.loadClass("org.scalafmt.Scalafmt"); - - Object configured; - - try { - // scalafmt >= 1.6.0 - Method parseHoconConfig = scalafmtCls.getMethod("parseHoconConfig", String.class); - - String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - - configured = parseHoconConfig.invoke(null, configStr); - } catch (NoSuchMethodException e) { - // scalafmt >= v0.7.0-RC1 && scalafmt < 1.6.0 - Method fromHocon = configCls.getMethod("fromHoconString", String.class, optionCls); - Object fromHoconEmptyPath = configCls.getMethod("fromHoconString$default$2").invoke(null); - - String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); - - configured = fromHocon.invoke(null, configStr, fromHoconEmptyPath); - } - - config = invokeNoArg(configured, "get"); - } - return input -> { - Object resultInsideFormatted = formatMethod.invoke(null, input, config, emptyRange); - return (String) formattedGet.invoke(resultInsideFormatted); - }; + final ClassLoader classLoader = jarState.getClassLoader(); + final Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.scalafmt.ScalafmtFormatterFunc"); + final Constructor constructor = formatterFunc.getConstructor(FileSignature.class); + return (FormatterFunc) constructor.newInstance(this.configSignature); } } - - private static Object invokeNoArg(Object obj, String toInvoke) throws Exception { - Class clazz = obj.getClass(); - Method method = clazz.getMethod(toInvoke); - return method.invoke(obj); - } } diff --git a/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java b/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java new file mode 100644 index 0000000000..6630bf32a1 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/shell/ShfmtStep.java @@ -0,0 +1,136 @@ +/* + * Copyright 2024 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.shell; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class ShfmtStep { + public static String name() { + return "shfmt"; + } + + public static String defaultVersion() { + return "3.8.0"; + } + + private final String version; + private final @Nullable String pathToExe; + + private ShfmtStep(String version, @Nullable String pathToExe) { + this.version = version; + this.pathToExe = pathToExe; + } + + public static ShfmtStep withVersion(String version) { + return new ShfmtStep(version, null); + } + + public ShfmtStep withPathToExe(String pathToExe) { + return new ShfmtStep(version, pathToExe); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createRoundtrip, RoundtripState::toEquality, EqualityState::toFunc); + } + + private RoundtripState createRoundtrip() throws IOException, InterruptedException { + String howToInstall = "" + + "You can download shfmt from https://github.com/mvdan/sh and " + + "then point Spotless to it with {@code pathToExe('/path/to/shfmt')} " + + "or you can use your platform's package manager:" + + "\n win: choco install shfmt" + + "\n mac: brew install shfmt" + + "\n linux: apt install shfmt" + + "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/673"; + final ForeignExe exe = ForeignExe.nameAndVersion("shfmt", version) + .pathToExe(pathToExe) + .versionRegex(Pattern.compile("([\\d.]+)")) + .fixCantFind(howToInstall) + .fixWrongVersion( + "You can tell Spotless to use the version you already have with {@code shfmt('{versionFound}')}" + + "or you can download the currently specified version, {version}.\n" + howToInstall); + return new RoundtripState(version, exe); + } + + static class RoundtripState implements Serializable { + private static final long serialVersionUID = 1L; + + final String version; + final ForeignExe exe; + + RoundtripState(String version, ForeignExe exe) { + this.version = version; + this.exe = exe; + } + + private EqualityState toEquality() { + return new EqualityState(version, exe); + } + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + static class EqualityState implements Serializable { + private static final long serialVersionUID = -1825662356883926318L; + + // used for up-to-date checks and caching + final String version; + final transient ForeignExe exe; + + // used for executing + private transient @Nullable List args; + + EqualityState(String version, ForeignExe pathToExe) { + this.version = version; + this.exe = Objects.requireNonNull(pathToExe); + } + + String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + if (args == null) { + // args will be reused during a single Spotless task execution, + // so this "prefix" is being "cached" for each Spotless format with shfmt. + args = List.of(exe.confirmVersionAndGetAbsolutePath(), "--filename"); + } + + // This will ensure that the next file name is retrieved on every format + final List finalArgs = Stream.concat(args.stream(), Stream.of(file.getAbsolutePath())) + .collect(Collectors.toList()); + + return runner.exec(input.getBytes(StandardCharsets.UTF_8), finalArgs).assertExitZero(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java index fece2ea0fd..e9c7a59e54 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package com.diffplug.spotless.sql; import java.io.File; -import java.io.Serializable; import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; @@ -32,24 +31,14 @@ public class DBeaverSQLFormatterStep { private DBeaverSQLFormatterStep() {} public static FormatterStep create(Iterable files) { - return FormatterStep.createLazy(NAME, - () -> new State(files), - State::createFormat); + return FormatterStep.create(NAME, FileSignature.promise(files), + FileSignature.Promised::get, + DBeaverSQLFormatterStep::createFormat); } - static final class State implements Serializable { - private static final long serialVersionUID = 1L; - - final FileSignature settingsSignature; - - State(final Iterable settingsFiles) throws Exception { - this.settingsSignature = FileSignature.signAsList(settingsFiles); - } - - FormatterFunc createFormat() throws Exception { - FormatterProperties preferences = FormatterProperties.from(settingsSignature.files()); - DBeaverSQLFormatter dbeaverSqlFormatter = new DBeaverSQLFormatter(preferences.getProperties()); - return dbeaverSqlFormatter::format; - } + private static FormatterFunc createFormat(FileSignature settings) { + FormatterProperties preferences = FormatterProperties.from(settings.files()); + DBeaverSQLFormatter dbeaverSqlFormatter = new DBeaverSQLFormatter(preferences.getProperties()); + return dbeaverSqlFormatter::format; } } diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java index 93c9afb5f8..d31b7520f7 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 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,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on DBPKeywordType from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java index f488bd5a00..58a0119a7f 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +22,11 @@ /** * **Warning:** Use this class at your own risk. It is an implementation detail and is not * guaranteed to exist in future versions. - * + *

* Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on SQLFormatterConfiguration from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java index c06ea5a1c0..4ab9f54a0b 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 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,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on Pair from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java index 2dc0b61aa0..698c87458f 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 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,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on SQLConstants from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java index 3c973885f6..daabd5386b 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on SQLDialect from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java index f2c98eebc1..01f0fd759e 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +25,11 @@ /** * **Warning:** Use this class at your own risk. It is an implementation detail and is not * guaranteed to exist in future versions. - * + *

* Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on SQLTokenizedFormatter from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java index 72080d22fc..b1931c3c4c 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on SQLTokensParser from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java index 5f727db571..d6d75faf9a 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 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,7 @@ * Forked from * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) - * + *

* Based on TokenType from https://github.com/serge-rider/dbeaver, * which itself is licensed under the Apache 2.0 license. */ diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java new file mode 100644 index 0000000000..166ee5f307 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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.yaml; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.diffplug.spotless.json.JacksonConfig; + +/** + * Specialization of {@link JacksonConfig} for YAML documents + */ +public class JacksonYamlConfig extends JacksonConfig { + private static final long serialVersionUID = 1L; + + protected Map yamlFeatureToToggle = new LinkedHashMap<>(); + + public Map getYamlFeatureToToggle() { + return Collections.unmodifiableMap(yamlFeatureToToggle); + } + + /** + * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature + */ + public void setYamlFeatureToToggle(Map yamlFeatureToToggle) { + this.yamlFeatureToToggle = yamlFeatureToToggle; + } + + /** + * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature + */ + public void appendYamlFeatureToToggle(Map features) { + this.yamlFeatureToToggle.putAll(features); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java new file mode 100644 index 0000000000..45c961b2e6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java @@ -0,0 +1,88 @@ +/* + * Copyright 2021-2024 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.yaml; + +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Provisioner; + +/** + * Simple YAML formatter which reformats the file according to Jackson YAMLFactory. + */ +// https://stackoverflow.com/questions/14515994/convert-json-string-to-pretty-print-json-output-using-jackson +// https://stackoverflow.com/questions/60891174/i-want-to-load-a-yaml-file-possibly-edit-the-data-and-then-dump-it-again-how +public class JacksonYamlStep implements Serializable { + 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; + private final JacksonYamlConfig jacksonConfig; + + private JacksonYamlStep(JarState.Promised jarState, JacksonYamlConfig jacksonConfig) { + this.jarState = jarState; + this.jacksonConfig = jacksonConfig; + } + + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + public static FormatterStep create(JacksonYamlConfig jacksonConfig, + String jacksonVersion, + Provisioner provisioner) { + Objects.requireNonNull(jacksonConfig, "jacksonConfig cannot be null"); + Objects.requireNonNull(provisioner, "provisioner cannot be null"); + return FormatterStep.create(NAME, + new JacksonYamlStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + jacksonVersion, provisioner)), jacksonConfig), + JacksonYamlStep::equalityState, + State::toFormatter); + } + + public static FormatterStep create(Provisioner provisioner) { + return create(new JacksonYamlConfig(), defaultVersion(), provisioner); + } + + private State equalityState() { + return new State(jarState.get(), jacksonConfig); + } + + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final JacksonYamlConfig jacksonConfig; + private final JarState jarState; + + State(JarState jarState, JacksonYamlConfig jacksonConfig) { + this.jarState = jarState; + this.jacksonConfig = jacksonConfig; + } + + FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, + InstantiationException, IllegalAccessException { + Class formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.yaml.JacksonYamlFormatterFunc"); + Constructor constructor = formatterFunc.getConstructor(JacksonYamlConfig.class); + return (FormatterFunc) constructor.newInstance(jacksonConfig); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/SerializeToByteArrayHack.java b/lib/src/main/java/com/diffplug/spotless/yaml/SerializeToByteArrayHack.java new file mode 100644 index 0000000000..c63b6d918b --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/yaml/SerializeToByteArrayHack.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 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.yaml; + +import java.io.File; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterStep; + +/** + * This step is a flag which marks that `ConfigurationCacheHackList` should + * serialize each item individually into `byte[]` array, rather than using normal + * serialization. + * + * The reason to use this is if you are using `toggleOffOn` *and* two kinds of + * google-java-format (e.g. one for format and the other for imports), then + * problems with Java's handling of object graphs will cause your up-to-date checks + * to always fail. `CombinedJavaFormatStepTest` recreates this situation. By adding + * this step, it will trigger this workaround which fixes the up-to-dateness bug. + * + * But, turning it on will break all `custom` steps that use Groovy closures. So + * by default you get regular serialization. If you're using `toggleOffOn` and having + * problems with up-to-dateness, then adding this step can be a workaround. + */ +public class SerializeToByteArrayHack implements FormatterStep { + private static final long serialVersionUID = 8071047581828362545L; + + @Override + public String getName() { + return "hack to force serializing objects to byte array"; + } + + @Nullable + @Override + public String format(String rawUnix, File file) throws Exception { + return null; + } + + @Override + public void close() throws Exception { + + } +} diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/common-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/common-serve.js new file mode 100644 index 0000000000..c1c9d62757 --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/common-serve.js @@ -0,0 +1,39 @@ +// this file will be glued to the top of the specific xy-serve.js file +const debug_serve = false; // set to true for debug log output in node process +const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager; +const express = require("express"); +const app = express(); + +app.use(express.json({ limit: "50mb" })); + +const fs = require("fs"); + +function debugLog() { + if (debug_serve) { + console.log.apply(this, arguments) + } +} + +var listener = app.listen(0, "127.0.0.1", () => { + debugLog("Server running on port " + listener.address().port); + fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) { + if (err) { + return console.log(err); + } else { + fs.rename("server.port.tmp", "server.port", function(err) { + if (err) { + return console.log(err); + } + }); // try to be as atomic as possible + } + }); +}); +const shutdownManager = new GracefulShutdownManager(listener); + +app.post("/shutdown", (req, res) => { + res.status(200).send("Shutting down"); + setTimeout(function() { + shutdownManager.terminate(() => debugLog("graceful shutdown finished.")); + }, 200); +}); + 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 new file mode 100644 index 0000000000..0d7ce930f7 --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/eslint-package.json @@ -0,0 +1,19 @@ +{ + "name": "spotless-eslint", + "version": "2.0.0", + "description": "Spotless formatter step for running eslint as a rest service.", + "repository": "https://github.com/diffplug/spotless", + "license": "Apache-2.0", + "scripts": { + "start": "node serve.js" + }, + "devDependencies": { +${devDependencies}, + "express": "4.18.2", + "@moebius/http-graceful-shutdown": "1.1.0" + }, + "dependencies": {}, + "engines": { + "node": ">=6" + } +} diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/eslint-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/eslint-serve.js new file mode 100644 index 0000000000..8b60e56dc8 --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/eslint-serve.js @@ -0,0 +1,74 @@ +const {ESLint} = require("eslint"); + +app.post("/eslint/format", async (req, res) => { + try { + const format_data = req.body; + + const ESLintOverrideConfig = format_data.eslint_override_config; + + const ESLintOverrideConfigFile = format_data.eslint_override_config_file; + + if (!ESLintOverrideConfig && !ESLintOverrideConfigFile) { + res.status(400).send("Error while formatting: No config provided"); + return; + } + + const filePath = format_data.file_path; + + if (!filePath) { + res.status(400).send("Error while formatting: No file path provided"); + return; + } + + const ESLintOptions = { + fix: true, + useEslintrc: false, // would result in (gradle) cache issues + }; + + if (format_data.ts_config_root_dir) { + ESLintOptions.baseConfig = { + parserOptions: { + tsconfigRootDir: format_data.ts_config_root_dir + } + }; + } + + + if (ESLintOverrideConfigFile) { + ESLintOptions.overrideConfigFile = ESLintOverrideConfigFile; + } + if (ESLintOverrideConfig) { + eval("ESLintOptions.overrideConfig = " + ESLintOverrideConfig); + } + + debugLog("using options: " + JSON.stringify(ESLintOptions)); + debugLog("format input: ", format_data.file_content); + + const eslint = new ESLint(ESLintOptions); + + + const lintTextOptions = { + filePath: filePath, + } + debugLog("lintTextOptions", lintTextOptions); + + // LintResult[] // https://eslint.org/docs/latest/developer-guide/nodejs-api#-lintresult-type + const results = await eslint.lintText(format_data.file_content, lintTextOptions); + if (results.length !== 1) { + res.status(500).send("Error while formatting: Unexpected number of results: " + JSON.stringify(results)); + return; + } + const result = results[0]; + debugLog("result: " + JSON.stringify(result)); + if (result.fatalErrorCount && result.fatalErrorCount > 0) { + res.status(500).send("Fatal error while formatting: " + JSON.stringify(result.messages)); + return; + } + const formatted = result.output || result.source || format_data.file_content; + res.set("Content-Type", "text/plain"); + res.send(formatted); + } catch (err) { + console.log("error", err); + res.status(500).send("Error while formatting: " + err); + } +}); 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 index 113f20bc3f..395a05da67 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json +++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json @@ -1,5 +1,5 @@ { - "name": "spotless-prettier-formatter-step", + "name": "spotless-prettier", "version": "2.0.0", "description": "Spotless formatter step for running prettier as a rest service.", "repository": "https://github.com/diffplug/spotless", @@ -9,7 +9,7 @@ }, "devDependencies": { ${devDependencies}, - "express": "4.17.1", + "express": "4.18.2", "@moebius/http-graceful-shutdown": "1.1.0" }, "dependencies": {}, diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js index 351ef73f9a..ac1f26790d 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js +++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js @@ -1,45 +1,15 @@ -const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager; -const express = require("express"); -const app = express(); - -app.use(express.json({ limit: "50mb" })); const prettier = require("prettier"); -const fs = require("fs"); - -var listener = app.listen(0, "127.0.0.1", () => { - console.log("Server running on port " + listener.address().port); - fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) { - if (err) { - return console.log(err); - } else { - fs.rename("server.port.tmp", "server.port", function(err) { - if (err) { - return console.log(err); - } - }); // try to be as atomic as possible - } - }); -}); -const shutdownManager = new GracefulShutdownManager(listener); - -app.post("/shutdown", (req, res) => { - res.status(200).send("Shutting down"); - setTimeout(function() { - shutdownManager.terminate(() => console.log("graceful shutdown finished.")); - }, 200); -}); - app.post("/prettier/config-options", (req, res) => { - var config_data = req.body; - var prettier_config_path = config_data.prettier_config_path; - var prettier_config_options = config_data.prettier_config_options || {}; + const config_data = req.body; + const prettier_config_path = config_data.prettier_config_path; + const prettier_config_options = config_data.prettier_config_options || {}; if (prettier_config_path) { prettier .resolveConfig(undefined, { config: prettier_config_path }) .then(options => { - var mergedConfigOptions = mergeConfigOptions(options, prettier_config_options); + const mergedConfigOptions = mergeConfigOptions(options, prettier_config_options); res.set("Content-Type", "application/json") res.json(mergedConfigOptions); }) @@ -50,21 +20,34 @@ app.post("/prettier/config-options", (req, res) => { res.json(prettier_config_options); }); -app.post("/prettier/format", (req, res) => { - var format_data = req.body; +app.post("/prettier/format", async (req, res) => { + const format_data = req.body; - var formatted_file_content = ""; + let formatted_file_content = ""; try { - formatted_file_content = prettier.format(format_data.file_content, format_data.config_options); + formatted_file_content = await prettierFormat(format_data.file_content, format_data.config_options); } catch(err) { - res.status(501).send("Error while formatting: " + err); + res.status(500).send("Error while formatting: " + err); return; } res.set("Content-Type", "text/plain"); res.send(formatted_file_content); }); -var mergeConfigOptions = function(resolved_config_options, config_options) { +const prettierFormat = async function(file_content, config_options) { + const result = prettier.format(file_content, config_options); + + // Check if result is a Promise (version 3.0.0 and above) + if (typeof result.then === 'function') { + return result; + } + + // If it's not a Promise (meaning it's a string), wrap it in a Promise (< 3.0.0) + return Promise.resolve(result); +} + + +const mergeConfigOptions = function(resolved_config_options, config_options) { if (resolved_config_options !== undefined && config_options !== undefined) { return extend(resolved_config_options, config_options); } @@ -76,15 +59,15 @@ var mergeConfigOptions = function(resolved_config_options, config_options) { } }; -var extend = function() { +const extend = function() { // Variables - var extended = {}; - var i = 0; - var length = arguments.length; + const extended = {}; + let i = 0; + const length = arguments.length; // Merge the object into the extended object - var merge = function(obj) { - for (var prop in obj) { + const merge = function (obj) { + for (const prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { extended[prop] = obj[prop]; } @@ -93,9 +76,8 @@ var extend = function() { // Loop through each object and conduct a merge for (; i < length; i++) { - var obj = arguments[i]; + const obj = arguments[i]; merge(obj); } - return extended; }; 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 index d6e5eff3b2..483dd0753d 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json +++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json @@ -1,5 +1,5 @@ { - "name": "spotless-tsfmt-formatter-step", + "name": "spotless-tsfmt", "version": "2.0.0", "description": "Spotless formatter step for running tsfmt as a rest service.", "repository": "https://github.com/diffplug/spotless", @@ -9,7 +9,7 @@ }, "devDependencies": { ${devDependencies}, - "express": "4.17.1", + "express": "4.18.2", "@moebius/http-graceful-shutdown": "1.1.0" }, "dependencies": {}, diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js index b25048d410..b9f20a1472 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js +++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js @@ -1,36 +1,5 @@ -const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager; -const express = require("express"); -const app = express(); -app.use(express.json({ limit: "50mb" })); - const tsfmt = require("typescript-formatter"); -const fs = require("fs"); - -var listener = app.listen(0, "127.0.0.1", () => { - console.log("Server running on port " + listener.address().port); - fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) { - if (err) { - return console.log(err); - } else { - fs.rename("server.port.tmp", "server.port", function(err) { - if (err) { - return console.log(err); - } - }); // try to be as atomic as possible - } - }); -}); - -const shutdownManager = new GracefulShutdownManager(listener); - -app.post("/shutdown", (req, res) => { - res.status(200).send("Shutting down"); - setTimeout(function() { - shutdownManager.terminate(() => console.log("graceful shutdown finished.")); - }, 200); -}); - app.post("/tsfmt/format", (req, res) => { var format_data = req.body; tsfmt.processString("spotless-format-string.ts", format_data.file_content, format_data.config_options).then(resultMap => { @@ -56,6 +25,6 @@ app.post("/tsfmt/format", (req, res) => { res.set("Content-Type", "text/plain"); res.send(resultMap.dest); }).catch(reason => { - res.status(501).send(reason); + res.status(500).send(reason); }); }); diff --git a/lib/src/palantirJavaFormat/java/com/diffplug/spotless/glue/pjf/PalantirJavaFormatFormatterFunc.java b/lib/src/palantirJavaFormat/java/com/diffplug/spotless/glue/pjf/PalantirJavaFormatFormatterFunc.java index 6824cdbc48..bdec215435 100644 --- a/lib/src/palantirJavaFormat/java/com/diffplug/spotless/glue/pjf/PalantirJavaFormatFormatterFunc.java +++ b/lib/src/palantirJavaFormat/java/com/diffplug/spotless/glue/pjf/PalantirJavaFormatFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 DiffPlug + * Copyright 2022-2024 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.glue.pjf; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import com.palantir.javaformat.java.Formatter; import com.palantir.javaformat.java.ImportOrderer; import com.palantir.javaformat.java.JavaFormatterOptions; @@ -26,16 +29,28 @@ public class PalantirJavaFormatFormatterFunc implements FormatterFunc { private final Formatter formatter; - public PalantirJavaFormatFormatterFunc() { - formatter = Formatter.createFormatter(JavaFormatterOptions.builder() - .style(JavaFormatterOptions.Style.PALANTIR) - .build()); + private final JavaFormatterOptions.Style formatterStyle; + + /** + * Creates a new formatter func that formats code via Palantir. + * @param style The style to use for formatting. + * @param formatJavadoc Whether to format Java docs. Requires at least Palantir 2.36.0 or later, otherwise the + * constructor will throw. + */ + public PalantirJavaFormatFormatterFunc(String style, boolean formatJavadoc) { + this.formatterStyle = JavaFormatterOptions.Style.valueOf(style); + JavaFormatterOptions.Builder builder = JavaFormatterOptions.builder(); + builder.style(formatterStyle); + if (formatJavadoc) { + applyFormatJavadoc(builder); + } + formatter = Formatter.createFormatter(builder.build()); } @Override public String apply(String input) throws Exception { String source = input; - source = ImportOrderer.reorderImports(source, JavaFormatterOptions.Style.PALANTIR); + source = ImportOrderer.reorderImports(source, formatterStyle); source = RemoveUnusedImports.removeUnusedImports(source); return formatter.formatSource(source); } @@ -44,4 +59,15 @@ public String apply(String input) throws Exception { public String toString() { return "PalantirJavaFormatFormatterFunc{formatter=" + formatter + '}'; } + + private static void applyFormatJavadoc(JavaFormatterOptions.Builder builder) { + // The formatJavadoc option is available since Palantir 2.36.0 + // To support older versions for now, attempt to invoke the builder method via reflection. + try { + Method formatJavadoc = JavaFormatterOptions.Builder.class.getMethod("formatJavadoc", boolean.class); + formatJavadoc.invoke(builder, true); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException("Cannot enable formatJavadoc option, make sure you are using Palantir with version 2.36.0 or later", e); + } + } } diff --git a/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java b/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java new file mode 100644 index 0000000000..6217ab6186 --- /dev/null +++ b/lib/src/scalafmt/java/com/diffplug/spotless/glue/scalafmt/ScalafmtFormatterFunc.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022-2024 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.scalafmt; + +import java.io.File; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.scalafmt.Scalafmt; +import org.scalafmt.config.ScalafmtConfig; +import org.scalafmt.config.ScalafmtConfig$; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; + +import scala.collection.immutable.Set$; + +public class ScalafmtFormatterFunc implements FormatterFunc.NeedsFile { + private final ScalafmtConfig config; + + public ScalafmtFormatterFunc(FileSignature configSignature) throws Exception { + if (configSignature.files().isEmpty()) { + // Note that reflection is used here only because Scalafmt has a method called + // default which happens to be a reserved Java keyword. The only way to call + // such methods is by reflection, see + // https://vlkan.com/blog/post/2015/11/20/scala-method-with-java-reserved-keyword/ + Method method = ScalafmtConfig$.MODULE$.getClass().getDeclaredMethod("default"); + config = (ScalafmtConfig) method.invoke(ScalafmtConfig$.MODULE$); + } else { + File file = configSignature.getOnlyFile(); + String configStr = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + config = Scalafmt.parseHoconConfig(configStr).get(); + } + } + + @Override + public String applyWithFile(String unix, File file) throws Exception { + return Scalafmt.format(unix, config, Set$.MODULE$.empty(), file.getAbsolutePath()).get(); + } +} diff --git a/lib/src/sortPom/java/com/diffplug/spotless/glue/pom/SortPomFormatterFunc.java b/lib/src/sortPom/java/com/diffplug/spotless/glue/pom/SortPomFormatterFunc.java index 4a72c48fa9..f5abef6614 100644 --- a/lib/src/sortPom/java/com/diffplug/spotless/glue/pom/SortPomFormatterFunc.java +++ b/lib/src/sortPom/java/com/diffplug/spotless/glue/pom/SortPomFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ package com.diffplug.spotless.glue.pom; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.logging.Logger; +import java.io.*; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.nio.file.Files; -import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.pom.SortPomCfg; @@ -30,7 +31,7 @@ import sortpom.parameter.PluginParameters; public class SortPomFormatterFunc implements FormatterFunc { - private static final Logger logger = Logger.getLogger(SortPomFormatterFunc.class.getName()); + private static final Logger logger = LoggerFactory.getLogger(SortPomFormatterFunc.class); private final SortPomCfg cfg; public SortPomFormatterFunc(SortPomCfg cfg) { @@ -39,39 +40,78 @@ public SortPomFormatterFunc(SortPomCfg cfg) { @Override public String apply(String input) throws Exception { - // SortPom expects a file to sort, so we write the inpout into a temporary file + // SortPom expects a file to sort, so we write the input into a temporary file File pom = File.createTempFile("pom", ".xml"); pom.deleteOnExit(); - IOUtils.write(input, new FileOutputStream(pom), cfg.encoding); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(pom, Charset.forName(cfg.encoding)))) { + writer.write(input); + } SortPomImpl sortPom = new SortPomImpl(); - sortPom.setup(new MySortPomLogger(), PluginParameters.builder() + PluginParameters.Builder builder = PluginParameters.builder() .setPomFile(pom) .setFileOutput(false, null, null, false) - .setEncoding(cfg.encoding) - .setFormatting(cfg.lineSeparator, cfg.expandEmptyElements, cfg.spaceBeforeCloseEmptyElement, cfg.keepBlankLines) - .setIndent(cfg.nrOfIndentSpace, cfg.indentBlankLines, cfg.indentSchemaLocation) + .setEncoding(cfg.encoding); + try { + builder = builder + .setFormatting(cfg.lineSeparator, cfg.expandEmptyElements, cfg.spaceBeforeCloseEmptyElement, + cfg.keepBlankLines, cfg.endWithNewline); + } catch (NoSuchMethodError e) { + try { + Method method = PluginParameters.Builder.class + .getMethod("setFormatting", String.class, boolean.class, boolean.class, boolean.class); + builder = (PluginParameters.Builder) method + .invoke(builder, cfg.lineSeparator, cfg.expandEmptyElements, cfg.spaceBeforeCloseEmptyElement, + cfg.keepBlankLines); + } catch (ReflectiveOperationException | RuntimeException ignore) { + throw e; + } + } + try { + builder = builder + .setIndent(cfg.nrOfIndentSpace, cfg.indentBlankLines, cfg.indentSchemaLocation, + cfg.indentAttribute); + } catch (NoSuchMethodError e) { + try { + Method method = PluginParameters.Builder.class + .getMethod("setIndent", int.class, boolean.class, boolean.class); + builder = (PluginParameters.Builder) method + .invoke(builder, cfg.nrOfIndentSpace, cfg.indentBlankLines, cfg.indentSchemaLocation); + } catch (ReflectiveOperationException | RuntimeException ignore) { + throw e; + } + } + builder = builder .setSortOrder(cfg.sortOrderFile, cfg.predefinedSortOrder) - .setSortEntities(cfg.sortDependencies, cfg.sortDependencyExclusions, cfg.sortPlugins, cfg.sortProperties, cfg.sortModules, cfg.sortExecutions) - .setTriggers(false) - .build()); + .setSortEntities(cfg.sortDependencies, cfg.sortDependencyExclusions, cfg.sortDependencyManagement, + cfg.sortPlugins, cfg.sortProperties, cfg.sortModules, cfg.sortExecutions) + .setIgnoreLineSeparators(false); + sortPom.setup(new MySortPomLogger(cfg.quiet), builder.build()); sortPom.sortPom(); - return IOUtils.toString(new FileInputStream(pom), cfg.encoding); + return Files.readString(pom.toPath(), Charset.forName(cfg.encoding)); } private static class MySortPomLogger implements SortPomLogger { + private final boolean quiet; + + public MySortPomLogger(boolean quiet) { + this.quiet = quiet; + } + @Override public void warn(String content) { - logger.warning(content); + logger.warn(content); } @Override public void info(String content) { - logger.info(content); + if (!quiet) { + logger.info(content); + } } @Override public void error(String content) { - logger.severe(content); + logger.error(content); } } } diff --git a/lib/src/test/java/com/diffplug/spotless/JarStateTest.java b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java new file mode 100644 index 0000000000..f44aa4a0b3 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/JarStateTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 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 java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.stream.Collectors; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class JarStateTest { + + @TempDir + java.nio.file.Path tempDir; + + File a; + + File b; + + Provisioner provisioner = (withTransitives, deps) -> deps.stream().map(name -> name.equals("a") ? a : b).collect(Collectors.toSet()); + + @BeforeEach + void setUp() throws IOException { + a = Files.createTempFile(tempDir, "a", ".class").toFile(); + Files.writeString(a.toPath(), "a"); + b = Files.createTempFile(tempDir, "b", ".class").toFile(); + Files.writeString(b.toPath(), "b"); + } + + @AfterEach + void tearDown() { + JarState.setForcedClassLoader(null); + } + + @Test + void itCreatesClassloaderWhenForcedClassLoaderNotSet() throws IOException { + JarState state1 = JarState.from(a.getName(), provisioner); + JarState state2 = JarState.from(b.getName(), provisioner); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(state1.getClassLoader()).isNotNull(); + softly.assertThat(state2.getClassLoader()).isNotNull(); + }); + } + + @Test + void itReturnsForcedClassloaderIfSetNoMatterIfSetBeforeOrAfterCreation() throws IOException { + JarState stateA = JarState.from(a.getName(), provisioner); + ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]); + JarState.setForcedClassLoader(forcedClassLoader); + JarState stateB = JarState.from(b.getName(), provisioner); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(stateA.getClassLoader()).isSameAs(forcedClassLoader); + softly.assertThat(stateB.getClassLoader()).isSameAs(forcedClassLoader); + }); + } + + @Test + void itReturnsForcedClassloaderEvenWhenRountripSerialized() throws IOException, ClassNotFoundException { + JarState stateA = JarState.from(a.getName(), provisioner); + ClassLoader forcedClassLoader = new URLClassLoader(new java.net.URL[0]); + JarState.setForcedClassLoader(forcedClassLoader); + JarState stateB = JarState.from(b.getName(), provisioner); + + JarState stateARoundtripSerialized = roundtripSerialize(stateA); + JarState stateBRoundtripSerialized = roundtripSerialize(stateB); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(stateARoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader); + softly.assertThat(stateBRoundtripSerialized.getClassLoader()).isSameAs(forcedClassLoader); + }); + } + + private JarState roundtripSerialize(JarState state) throws IOException, ClassNotFoundException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ObjectOutputStream oOut = new ObjectOutputStream(outputStream)) { + oOut.writeObject(state); + } + try (ObjectInputStream oIn = new ObjectInputStream(new java.io.ByteArrayInputStream(outputStream.toByteArray()))) { + return (JarState) oIn.readObject(); + } + } + +} diff --git a/lib/src/test/java/com/diffplug/spotless/LineEndingTest.java b/lib/src/test/java/com/diffplug/spotless/LineEndingTest.java new file mode 100644 index 0000000000..e84d660758 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/LineEndingTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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 java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LineEndingTest { + + @Test + void testGetEndingFor() throws IOException { + assertLineEnding("\r", "\r"); + assertLineEnding("\r", "Test\r"); + assertLineEnding("\r", "Test\rTest2\n"); + + assertLineEnding("\n", "Test"); + + assertLineEnding("\r\n", "\r\n"); + assertLineEnding("\r\n", "Test\r\n"); + assertLineEnding("\r\n", "Test\r\nTest2\n"); + + assertLineEnding("\n", "\n"); + assertLineEnding("\n", "Test\n"); + assertLineEnding("\n", "Test\nTest2\r"); + assertLineEnding("\n", "\n\t"); + } + + static void assertLineEnding(String ending, String input) throws IOException { + try (Reader reader = new StringReader(input)) { + Assertions.assertEquals(ending, LineEnding.PreserveLineEndingPolicy.getEndingFor(reader)); + } + } +} diff --git a/lib/src/test/java/com/diffplug/spotless/LintSuppressionTest.java b/lib/src/test/java/com/diffplug/spotless/LintSuppressionTest.java new file mode 100644 index 0000000000..1e1037a6ef --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/LintSuppressionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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 java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.assertj.core.api.AbstractBooleanAssert; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.generic.EndWithNewlineStep; + +public class LintSuppressionTest { + private LintState dummyLintState() { + var lints = new ArrayList(); + lints.add(Lint.atLine(2, "66", "Order 66")); + var perStep = new ArrayList>(); + perStep.add(lints); + return new LintState(DirtyState.clean(), perStep); + } + + private Formatter formatter() { + return Formatter.builder() + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .steps(List.of(EndWithNewlineStep.create())) + .build(); + } + + @Test + public void testMatchSingle() { + var noSuppressions = dummyLintState().withRemovedSuppressions(formatter(), "file", List.of()); + assertThat(noSuppressions.isHasLints()).isTrue(); + removesLint(s -> s.setStep("blah")).isFalse(); + removesLint(s -> s.setStep("endWithNewline")).isTrue(); + removesLint(s -> s.setPath("blah")).isFalse(); + removesLint(s -> s.setPath("testFile")).isTrue(); + removesLint(s -> s.setShortCode("blah")).isFalse(); + removesLint(s -> s.setShortCode("66")).isTrue(); + } + + @Test + public void testMatchDouble() { + removesLint(s -> { + s.setStep("endWithNewline"); + s.setShortCode("blah"); + }).isFalse(); + removesLint(s -> { + s.setStep("blah"); + s.setShortCode("66"); + }).isFalse(); + removesLint(s -> { + s.setStep("endWithNewline"); + s.setShortCode("66"); + }).isTrue(); + } + + private AbstractBooleanAssert removesLint(Consumer suppression) { + var s = new LintSuppression(); + suppression.accept(s); + + var ls = dummyLintState().withRemovedSuppressions(formatter(), "testFile", List.of(s)); + return assertThat(!ls.isHasLints()); + } +} diff --git a/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java b/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java new file mode 100644 index 0000000000..94fa49dbc1 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2023 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.stream.Stream; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class RingBufferByteArrayOutputStreamTest { + + private final byte[] bytes = new byte[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toStringBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toString()).isEqualTo("0123456789"); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toStringBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toString()).hasSize(4); + Assertions.assertThat(stream.toString()).isEqualTo("6789"); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toStringBehavesNormallyAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toString()).isEqualTo("0123456789"); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toByteArrayBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toByteArray()).isEqualTo(bytes); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toByteArrayBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toByteArray()).hasSize(4); + Assertions.assertThat(stream.toByteArray()).isEqualTo(new byte[]{'6', '7', '8', '9'}); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void toByteArrayBehavesOverwritingAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1); + writeStrategy.write(stream, bytes); + Assertions.assertThat(stream.toByteArray()).isEqualTo(bytes); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void writeToBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) throws IOException { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1); + writeStrategy.write(stream, bytes); + ByteArrayOutputStream target = new ByteArrayOutputStream(); + stream.writeTo(target); + Assertions.assertThat(target.toByteArray()).isEqualTo(bytes); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void writeToBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) throws IOException { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1); + writeStrategy.write(stream, bytes); + ByteArrayOutputStream target = new ByteArrayOutputStream(); + stream.writeTo(target); + Assertions.assertThat(target.toByteArray()).hasSize(4); + Assertions.assertThat(target.toByteArray()).isEqualTo(new byte[]{'6', '7', '8', '9'}); + } + + @ParameterizedTest(name = "{index} writeStrategy: {0}") + @MethodSource("writeStrategies") + void writeToBehavesNormallyAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) throws IOException { + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1); + writeStrategy.write(stream, bytes); + ByteArrayOutputStream target = new ByteArrayOutputStream(); + stream.writeTo(target); + Assertions.assertThat(target.toByteArray()).isEqualTo(bytes); + } + + @Test + void writeToBehavesCorrectlyWhenOverLimitMultipleCalls() { + // this test explicitly captures a border case where the buffer is not empty but can exactly fit what we are writing + RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(2, 1); + stream.write('0'); + stream.write(new byte[]{'1', '2'}, 0, 2); + Assertions.assertThat(stream.toString()).hasSize(2); + Assertions.assertThat(stream.toString()).isEqualTo("12"); + } + + private static Stream writeStrategies() { + return Stream.of( + Arguments.of("writeAllAtOnce", allAtOnce()), + Arguments.of("writeOneByteAtATime", oneByteAtATime()), + Arguments.of("writeTwoBytesAtATime", twoBytesAtATime()), + Arguments.of("writeOneAndThenTwoBytesAtATime", oneAndThenTwoBytesAtATime()), + Arguments.of("firstFourBytesAndThenTheRest", firstFourBytesAndThenTheRest())); + } + + private static ByteWriteStrategy allAtOnce() { + return (stream, bytes) -> stream.write(bytes, 0, bytes.length); + } + + private static ByteWriteStrategy oneByteAtATime() { + return (stream, bytes) -> { + for (byte b : bytes) { + stream.write(b); + } + }; + } + + private static ByteWriteStrategy twoBytesAtATime() { + return (stream, bytes) -> { + for (int i = 0; i < bytes.length; i += 2) { + stream.write(bytes, i, 2); + } + }; + } + + private static ByteWriteStrategy oneAndThenTwoBytesAtATime() { + return (stream, bytes) -> { + int written = 0; + for (int i = 0; i + 3 < bytes.length; i += 3) { + stream.write(bytes, i, 1); + stream.write(bytes, i + 1, 2); + written += 3; + } + if (written < bytes.length) { + stream.write(bytes, written, bytes.length - written); + } + + }; + } + + private static ByteWriteStrategy firstFourBytesAndThenTheRest() { + return (stream, bytes) -> { + stream.write(bytes, 0, 4); + stream.write(bytes, 4, bytes.length - 4); + }; + } + + @FunctionalInterface + private interface ByteWriteStrategy { + void write(RingBufferByteArrayOutputStream stream, byte[] bytes); + } + +} diff --git a/lib/src/test/java/com/diffplug/spotless/npm/TimedLoggerTest.java b/lib/src/test/java/com/diffplug/spotless/npm/TimedLoggerTest.java new file mode 100644 index 0000000000..f08d4f1622 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/npm/TimedLoggerTest.java @@ -0,0 +1,269 @@ +/* + * Copyright 2023 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 com.diffplug.spotless.npm.TimedLogger.MESSAGE_PREFIX_BEGIN; +import static com.diffplug.spotless.npm.TimedLogger.MESSAGE_PREFIX_END; +import static com.diffplug.spotless.npm.TimedLogger.MESSAGE_SUFFIX_TOOK; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.helpers.LegacyAbstractLogger; + +import com.diffplug.spotless.npm.TimedLogger.TestTicker; + +class TimedLoggerTest { + + private TestLogger testLogger; + + private TestTicker testTicker; + + private TimedLogger timedLogger; + + @BeforeEach + void setUp() { + testLogger = new TestLogger(); + testTicker = new TestTicker(); + timedLogger = TimedLogger.forLogger(testLogger, testTicker); + } + + @Test + void itDoesNotLogWhenLevelDisabled() { + + TestLogger logger = new TestLogger() { + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + }; + TimedLogger timedLogger = TimedLogger.forLogger(logger); + + timedLogger.withInfo("This should not be logged").run(() -> Thread.sleep(1)); + logger.assertNoEvents(); + } + + @Test + void itLogsMillisWhenTakingMillis() { + timedLogger.withInfo("This should be logged").run(() -> testTicker.tickMillis(999)); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_SUFFIX_TOOK, "999ms"); + } + + @Test + void itLogsSecondsOnlyWhenTakingSeconds() { + timedLogger.withInfo("This should be logged").run(() -> testTicker.tickMillis(2_000)); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_SUFFIX_TOOK, "2.0s"); + } + + @Test + void itLogsMinutesOnlyWhenTakingMinutes() { + timedLogger.withInfo("This should be logged").run(() -> testTicker.tickMillis(2 * 60 * 1_000)); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_SUFFIX_TOOK, "2m"); + } + + @Test + void itLogsMinutesAndSecondsWhenTakingMinutesAndSeconds() { + timedLogger.withInfo("This should be logged").run(() -> testTicker.tickMillis(2 * 60 * 1_000 + 3 * 1_000)); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_SUFFIX_TOOK, "2m 3.0s"); + } + + @Test + void itLogsBeginAndEndPrefixes() { + timedLogger.withInfo("This should be logged").run(() -> testTicker.tickMillis(1)); + + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_BEGIN); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_END, "1ms"); + } + + @Test + void itThrowsExceptionsInChecked() { + Assertions.assertThatThrownBy(() -> timedLogger.withInfo("This should be logged").runChecked(() -> { + throw new Exception("This is an exception"); + })).isInstanceOf(Exception.class).hasMessage("This is an exception"); + } + + @Test + void itLogsEvenWhenExceptionsAreThrown() { + Assertions.assertThatThrownBy(() -> timedLogger.withInfo("This should be logged").run(() -> { + testTicker.tickMillis(2); + throw new Exception("This is an exception"); + })).isInstanceOf(RuntimeException.class) + .hasMessageContaining("This is an exception") + .hasCauseInstanceOf(Exception.class); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_BEGIN); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_END, "2ms"); + } + + @Test + void itReturnsValueOfCallableWhileStillLogging() { + String result = timedLogger.withInfo("This should be logged").call(() -> { + testTicker.tickMillis(2); + return "This is the result"; + }); + + Assertions.assertThat(result).isEqualTo("This is the result"); + + testLogger.assertEvents(2); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_BEGIN); + testLogger.assertHasEventWithMessageAndArguments(MESSAGE_PREFIX_END, "2ms"); + } + + private static class TestLogger extends LegacyAbstractLogger { + + private final List events = new LinkedList<>(); + + @Override + protected String getFullyQualifiedCallerName() { + return TestLogger.class.getName(); + } + + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String msg, Object[] arguments, Throwable throwable) { + events.add(new TestLoggingEvent(level, marker, msg, arguments, throwable)); + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public boolean isDebugEnabled() { + return true; + } + + @Override + public boolean isInfoEnabled() { + return true; + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public boolean isErrorEnabled() { + return true; + } + + public List getEvents() { + return events; + } + + public void assertNoEvents() { + Assertions.assertThat(getEvents()).isEmpty(); + } + + public void assertEvents(int eventCount) { + Assertions.assertThat(getEvents()).hasSize(eventCount); + } + + public void assertHasEventWithMessageAndArguments(String message, Object... arguments) { + + Assertions.assertThat(getEvents()).haveAtLeastOne(new Condition<>(event -> { + if (!event.msg().contains(message)) { + return false; + } + if (event.arguments().length != arguments.length) { + return false; + } + for (int i = 0; i < arguments.length; i++) { + if (!String.valueOf(event.arguments()[i]).equals(arguments[i])) { + return false; + } + } + return true; + }, "Event with message containing '%s' and arguments '%s'", message, Arrays.toString(arguments))); + } + } + + private static class TestLoggingEvent { + + private final Level level; + private final Marker marker; + private final String msg; + private final Object[] arguments; + private final Throwable throwable; + + public TestLoggingEvent(Level level, Marker marker, String msg, Object[] arguments, Throwable throwable) { + this.level = level; + this.marker = marker; + this.msg = msg; + this.arguments = arguments; + this.throwable = throwable; + } + + public Level level() { + return level; + } + + public Marker marker() { + return marker; + } + + public String msg() { + return msg; + } + + public Object[] arguments() { + return arguments; + } + + public Throwable throwable() { + return throwable; + } + + @Override + public String toString() { + return String.format( + "TestLoggingEvent[level=%s, marker=%s, msg=%s, arguments=%s, throwable=%s]", + this.level, + this.marker, + this.msg, + Arrays.toString(this.arguments), + this.throwable); + } + } + +} diff --git a/lib/src/testCompatCleanthat2Dot1/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFuncTest.java b/lib/src/testCompatCleanthat2Dot1/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFuncTest.java new file mode 100644 index 0000000000..2f3cdc9646 --- /dev/null +++ b/lib/src/testCompatCleanthat2Dot1/java/com/diffplug/spotless/glue/java/JavaCleanthatRefactorerFuncTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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.java; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import eu.solven.cleanthat.engine.java.refactorer.JavaRefactorer; + +public class JavaCleanthatRefactorerFuncTest { + @Test + public void testMutatorsDetection() { + Assertions.assertThat(JavaRefactorer.getAllIncluded()).isNotEmpty(); + } +} diff --git a/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java new file mode 100644 index 0000000000..f9c19d69a7 --- /dev/null +++ b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class KtLintCompat0Dot48Dot0AdapterTest { + @Test + public void testDefaults(@TempDir Path path) throws IOException { + KtLintCompat0Dot48Dot0Adapter ktLintCompat0Dot48Dot0Adapter = new KtLintCompat0Dot48Dot0Adapter(); + var content = loadAndWriteText(path, "empty_class_body.kt"); + final Path filePath = Paths.get(path.toString(), "empty_class_body.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + + String formatted = ktLintCompat0Dot48Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class empty_class_body\n", formatted); + } + + @Test + public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { + KtLintCompat0Dot48Dot0Adapter ktLintCompat0Dot48Dot0Adapter = new KtLintCompat0Dot48Dot0Adapter(); + var content = loadAndWriteText(path, "fails_no_semicolons.kt"); + final Path filePath = Paths.get(path.toString(), "fails_no_semicolons.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + editorConfigOverrideMap.put("indent_style", "tab"); + editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); + // ktlint_filename is an invalid rule in ktlint 0.48.0 + editorConfigOverrideMap.put("ktlint_filename", "disabled"); + + String formatted = ktLintCompat0Dot48Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class fails_no_semicolons {\n\tval i = 0;\n}\n", formatted); + } + + private static String loadAndWriteText(Path path, String name) throws IOException { + try (InputStream is = KtLintCompat0Dot48Dot0AdapterTest.class.getResourceAsStream("/" + name)) { + Files.copy(is, path.resolve(name)); + } + return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8); + } + +} diff --git a/lib/src/testCompatKtLint0Dot48Dot0/resources/empty_class_body.kt b/lib/src/testCompatKtLint0Dot48Dot0/resources/empty_class_body.kt new file mode 100644 index 0000000000..b84774d572 --- /dev/null +++ b/lib/src/testCompatKtLint0Dot48Dot0/resources/empty_class_body.kt @@ -0,0 +1,3 @@ +class empty_class_body { + +} diff --git a/lib/src/testCompatKtLint0Dot48Dot0/resources/fails_no_semicolons.kt b/lib/src/testCompatKtLint0Dot48Dot0/resources/fails_no_semicolons.kt new file mode 100644 index 0000000000..20ab460913 --- /dev/null +++ b/lib/src/testCompatKtLint0Dot48Dot0/resources/fails_no_semicolons.kt @@ -0,0 +1,3 @@ +class fails_no_semicolons { + val i = 0; +} diff --git a/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java new file mode 100644 index 0000000000..27d085f7fd --- /dev/null +++ b/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class KtLintCompat0Dot49Dot0AdapterTest { + @Test + public void testDefaults(@TempDir Path path) throws IOException { + KtLintCompat0Dot49Dot0Adapter ktLintCompat0Dot49Dot0Adapter = new KtLintCompat0Dot49Dot0Adapter(); + var content = loadAndWriteText(path, "EmptyClassBody.kt"); + final Path filePath = Paths.get(path.toString(), "EmptyClassBody.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + + String formatted = ktLintCompat0Dot49Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class EmptyClassBody\n", formatted); + } + + @Test + public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { + KtLintCompat0Dot49Dot0Adapter ktLintCompat0Dot49Dot0Adapter = new KtLintCompat0Dot49Dot0Adapter(); + var content = loadAndWriteText(path, "FailsNoSemicolons.kt"); + final Path filePath = Paths.get(path.toString(), "FailsNoSemicolons.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + editorConfigOverrideMap.put("indent_style", "tab"); + editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); + + String formatted = ktLintCompat0Dot49Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class FailsNoSemicolons {\n\tval i = 0;\n}\n", formatted); + } + + private static String loadAndWriteText(Path path, String name) throws IOException { + try (InputStream is = KtLintCompat0Dot49Dot0AdapterTest.class.getResourceAsStream("/" + name)) { + Files.copy(is, path.resolve(name)); + } + return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8); + } + +} diff --git a/lib/src/testCompatKtLint0Dot49Dot0/resources/EmptyClassBody.kt b/lib/src/testCompatKtLint0Dot49Dot0/resources/EmptyClassBody.kt new file mode 100644 index 0000000000..7da53fb78d --- /dev/null +++ b/lib/src/testCompatKtLint0Dot49Dot0/resources/EmptyClassBody.kt @@ -0,0 +1,3 @@ +class EmptyClassBody { + +} diff --git a/lib/src/testCompatKtLint0Dot49Dot0/resources/FailsNoSemicolons.kt b/lib/src/testCompatKtLint0Dot49Dot0/resources/FailsNoSemicolons.kt new file mode 100644 index 0000000000..4cf05ceacf --- /dev/null +++ b/lib/src/testCompatKtLint0Dot49Dot0/resources/FailsNoSemicolons.kt @@ -0,0 +1,3 @@ +class FailsNoSemicolons { + val i = 0; +} diff --git a/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java new file mode 100644 index 0000000000..27dce68f09 --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class KtLintCompat0Dot50Dot0AdapterTest { + @Test + public void testDefaults(@TempDir Path path) throws IOException { + KtLintCompat0Dot50Dot0Adapter KtLintCompat0Dot50Dot0Adapter = new KtLintCompat0Dot50Dot0Adapter(); + var content = loadAndWriteText(path, "EmptyClassBody.kt"); + final Path filePath = Paths.get(path.toString(), "EmptyClassBody.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + + String formatted = KtLintCompat0Dot50Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class EmptyClassBody\n", formatted); + } + + @Test + public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { + KtLintCompat0Dot50Dot0Adapter KtLintCompat0Dot50Dot0Adapter = new KtLintCompat0Dot50Dot0Adapter(); + var content = loadAndWriteText(path, "FailsNoSemicolons.kt"); + final Path filePath = Paths.get(path.toString(), "FailsNoSemicolons.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + editorConfigOverrideMap.put("indent_style", "tab"); + editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); + + String formatted = KtLintCompat0Dot50Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class FailsNoSemicolons {\n\tval i = 0;\n}\n", formatted); + } + + private static String loadAndWriteText(Path path, String name) throws IOException { + try (InputStream is = KtLintCompat0Dot50Dot0AdapterTest.class.getResourceAsStream("/" + name)) { + Files.copy(is, path.resolve(name)); + } + return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8); + } + +} diff --git a/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt b/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt new file mode 100644 index 0000000000..7da53fb78d --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt @@ -0,0 +1,3 @@ +class EmptyClassBody { + +} diff --git a/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt b/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt new file mode 100644 index 0000000000..4cf05ceacf --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt @@ -0,0 +1,3 @@ +class FailsNoSemicolons { + val i = 0; +} diff --git a/lib/src/testCompatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0AdapterTest.java b/lib/src/testCompatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0AdapterTest.java new file mode 100644 index 0000000000..760aa89ae5 --- /dev/null +++ b/lib/src/testCompatKtLint1Dot0Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat1Dot0Dot0AdapterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023-2024 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.ktlint.compat; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class KtLintCompat1Dot0Dot0AdapterTest { + @Test + public void testDefaults(@TempDir Path path) throws IOException { + KtLintCompat1Dot0Dot0Adapter KtLintCompat1Dot0Dot0Adapter = new KtLintCompat1Dot0Dot0Adapter(); + var content = loadAndWriteText(path, "EmptyClassBody.kt"); + final Path filePath = Paths.get(path.toString(), "EmptyClassBody.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + + String formatted = KtLintCompat1Dot0Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class EmptyClassBody\n", formatted); + } + + @Test + public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { + KtLintCompat1Dot0Dot0Adapter KtLintCompat1Dot0Dot0Adapter = new KtLintCompat1Dot0Dot0Adapter(); + var content = loadAndWriteText(path, "FailsNoSemicolons.kt"); + final Path filePath = Paths.get(path.toString(), "FailsNoSemicolons.kt"); + + Map editorConfigOverrideMap = new HashMap<>(); + editorConfigOverrideMap.put("indent_style", "tab"); + editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); + + String formatted = KtLintCompat1Dot0Dot0Adapter.format(content, filePath, null, editorConfigOverrideMap); + assertEquals("class FailsNoSemicolons {\n\tval i = 0;\n}\n", formatted); + } + + private static String loadAndWriteText(Path path, String name) throws IOException { + try (InputStream is = KtLintCompat1Dot0Dot0AdapterTest.class.getResourceAsStream("/" + name)) { + Files.copy(is, path.resolve(name)); + } + return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8); + } + +} diff --git a/lib/src/testCompatKtLint1Dot0Dot0/resources/EmptyClassBody.kt b/lib/src/testCompatKtLint1Dot0Dot0/resources/EmptyClassBody.kt new file mode 100644 index 0000000000..7da53fb78d --- /dev/null +++ b/lib/src/testCompatKtLint1Dot0Dot0/resources/EmptyClassBody.kt @@ -0,0 +1,3 @@ +class EmptyClassBody { + +} diff --git a/lib/src/testCompatKtLint1Dot0Dot0/resources/FailsNoSemicolons.kt b/lib/src/testCompatKtLint1Dot0Dot0/resources/FailsNoSemicolons.kt new file mode 100644 index 0000000000..4cf05ceacf --- /dev/null +++ b/lib/src/testCompatKtLint1Dot0Dot0/resources/FailsNoSemicolons.kt @@ -0,0 +1,3 @@ +class FailsNoSemicolons { + val i = 0; +} diff --git a/lib/src/zjsonPatch/java/com/diffplug/spotless/glue/json/JsonPatchFormatterFunc.java b/lib/src/zjsonPatch/java/com/diffplug/spotless/glue/json/JsonPatchFormatterFunc.java new file mode 100644 index 0000000000..dd2f053791 --- /dev/null +++ b/lib/src/zjsonPatch/java/com/diffplug/spotless/glue/json/JsonPatchFormatterFunc.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.flipkart.zjsonpatch.JsonPatch; + +import com.diffplug.spotless.FormatterFunc; + +public class JsonPatchFormatterFunc implements FormatterFunc { + private final ObjectMapper objectMapper; + private final List> patch; + private final String patchString; + + public JsonPatchFormatterFunc(String patchString) { + this.objectMapper = new ObjectMapper(); + this.patch = null; + this.patchString = patchString; + } + + public JsonPatchFormatterFunc(List> patch) { + this.objectMapper = new ObjectMapper(); + this.patch = patch; + this.patchString = null; + } + + @Override + public String apply(String input) throws Exception { + var patchNode = this.patch == null + ? objectMapper.readTree(patchString) + : objectMapper.valueToTree(patch); + + var inputNode = objectMapper.readTree(input); + + var patchedNode = JsonPatch.apply(patchNode, inputNode); + + return objectMapper.writeValueAsString(patchedNode); + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 4d9d131e82..1ddd31cc52 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,10 +3,492 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] + +## [7.0.3] - 2025-04-07 +### Changed +* Use palantir-java-format 2.57.0 on Java 21. ([#2447](https://github.com/diffplug/spotless/pull/2447)) +* Re-try `npm install` with `--prefer-online` after `ERESOLVE` error. ([#2448](https://github.com/diffplug/spotless/pull/2448)) +* Apply Gradle's strict plugin types validation to the Spotless plugin. ([#2454](https://github.com/diffplug/spotless/pull/2454)) + +## [7.0.2] - 2025-01-14 +### Fixed +* Node.JS-based tasks now work with the configuration cache ([#2372](https://github.com/diffplug/spotless/issues/2372)) +* Eclipse-based tasks can now handle parallel configuration ([#2389](https://github.com/diffplug/spotless/issues/2389)) + +## [7.0.1] - 2025-01-07 +### Fixed +* Deployment was missing part of the CDT formatter, now fixed. ([#2384](https://github.com/diffplug/spotless/issues/2384)) + +## [7.0.0] - 2025-01-06 +## Headline changes +* The long `7.0.0.BETAX` period is finally over, Spotless for Gradle 7.0 is here! +* Full, no asterisk support for configuration cache (end of [#987](https://github.com/diffplug/spotless/issues/987)) +* Spotless now supports [linting](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#lints) in addition to formatting. +### Changed +* Allow setting Eclipse config from a string, not only from files ([#2337](https://github.com/diffplug/spotless/pull/2337)) +* Bump default `ktlint` version to latest `1.3.0` -> `1.4.0`. ([#2314](https://github.com/diffplug/spotless/pull/2314)) +* Bump default `jackson` version to latest `2.18.0` -> `2.18.1`. ([#2319](https://github.com/diffplug/spotless/pull/2319)) +* Bump default `ktfmt` version to latest `0.52` -> `0.53`. ([#2320](https://github.com/diffplug/spotless/pull/2320)) +* Bump default `ktlint` version to latest `1.4.0` -> `1.5.0`. ([#2354](https://github.com/diffplug/spotless/pull/2354)) +* Bump minimum `eclipse-cdt` version to `11.0` (removed support for `10.7`). ([#2373](https://github.com/diffplug/spotless/pull/2373)) +* Bump default `eclipse` version to latest `4.32` -> `4.34`. ([#2381](https://github.com/diffplug/spotless/pull/2381)) +### Fixed +* `toggleOffOn` now works with the configuration cache. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317)) +* Using `custom` with a Groovy closure now works with and without configuration cache. ([#2376](https://github.com/diffplug/spotless/pull/2376)) + * Minimum required Gradle version for this to work has bumped from `8.0` to `8.4`. + * The global git system config is now ignored for line-ending purposes. + * Added `SerializeToByteArrayHack` as a flag for a limitation at the intersection of `toggleOffOn` and `custom`. +* You can now use `removeUnusedImports` and `googleJavaFormat` at the same time again. (fixes [#2159](https://github.com/diffplug/spotless/issues/2159)) +* The default list of type annotations used by `formatAnnotations` now includes Jakarta Validation's `Valid` and constraints validations (fixes [#2334](https://github.com/diffplug/spotless/issues/2334)) +* `indentWith[Spaces|Tabs]` has been deprecated in favor of `leadingTabsToSpaces` and `leadingSpacesToTabs`. ([#2350](https://github.com/diffplug/spotless/pull/2350) fixes [#794](https://github.com/diffplug/spotless/issues/794)) + +## [7.0.0.BETA4] - 2024-10-24 +### Added +* Support for line ending policy `PRESERVE` which just takes the first line ending of every given file as setting (no matter if `\n`, `\r\n` or `\r`) ([#2304](https://github.com/diffplug/spotless/pull/2304)) +* New `suppressLintsFor` DSL ([docs](https://github.com/diffplug/spotless/tree/main/plugin-gradle#linting)) ([#2307](https://github.com/diffplug/spotless/pull/2307)) + * `ignoreErrorForStep` and `ignoreErrorForPath` are now deprecated aliases of `suppressLintsFor` + * Spotless is still a formatter not a linter, it just models formatting failures as lints rather than stopping execution (resolves [#287](https://github.com/diffplug/spotless/issues/287)) +* Add _Sort Members_ feature based on [Eclipse JDT](README.md#eclipse-jdt) implementation. ([#2312](https://github.com/diffplug/spotless/pull/2312)) +### Fixed +* `ktlint` steps now read from the `string` instead of the `file` so they don't clobber earlier steps. (fixes [#1599](https://github.com/diffplug/spotless/issues/1599)) + +## [7.0.0.BETA3] - 2024-10-15 +### Added +### Changed +* Use the Gradle user home directory by default for the download directory for the biome executable. Previously, the + plugin tried to use Maven's home directory, which is not always accessible by a Gradle plugin. ([#2187](https://github.com/diffplug/spotless/issues/2187)) +* Add explicit support for CSS via biome. Formatting CSS via biome was already supported as a general + formatting step. Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). + ([#2259](https://github.com/diffplug/spotless/pull/2259)) +### Changed +* Bump default `buf` version to latest `1.24.0` -> `1.44.0`. ([#2291](https://github.com/diffplug/spotless/pull/2291)) +* Bump default `google-java-format` version to latest `1.23.0` -> `1.24.0`. ([#2294](https://github.com/diffplug/spotless/pull/2294)) +* Bump default `jackson` version to latest `2.17.2` -> `2.18.0`. ([#2279](https://github.com/diffplug/spotless/pull/2279)) +* Bump default `cleanthat` version to latest `2.21` -> `2.22`. ([#2296](https://github.com/diffplug/spotless/pull/2296)) +### Fixed +* Java import order, ignore duplicate group entries. ([#2293](https://github.com/diffplug/spotless/pull/2293)) +* Remote build cache shouldn't have cache misses anymore. ([#2298](https://github.com/diffplug/spotless/pull/2298) fixes [#2168](https://github.com/diffplug/spotless/issues/2168)) + +## [7.0.0.BETA2] - 2024-08-25 +### Changed +* Support toning down sortPom logging. ([#2185](https://github.com/diffplug/spotless/pull/2185)) +* Bump default `ktlint` version to latest `1.2.1` -> `1.3.0`. ([#2165](https://github.com/diffplug/spotless/pull/2165)) +* Bump default `ktfmt` version to latest `0.49` -> `0.52`. ([#2172](https://github.com/diffplug/spotless/pull/2172), [#2231](https://github.com/diffplug/spotless/pull/2231)) +* Rename property `ktfmt` option `removeUnusedImport` -> `removeUnusedImports` to match `ktfmt`. ([#2172](https://github.com/diffplug/spotless/pull/2172)) +* Bump default `eclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `greclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179), [#2190](https://github.com/diffplug/spotless/pull/2190)) +* Bump default `cdt` version to latest `11.3` -> `11.6`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `gson` version to latest `2.10.1` -> `2.11.0`. ([#2128](https://github.com/diffplug/spotless/pull/2128)) +* Bump default `cleanthat` version to latest `2.20` -> `2.21`. ([#2210](https://github.com/diffplug/spotless/pull/2210)) +* Bump default `google-java-format` version to latest `1.22.0` -> `1.23.0`. ([#2212](https://github.com/diffplug/spotless/pull/2212)) +### Fixed +* Fix compatibility issue introduced by `ktfmt` `0.51`. ([#2172](https://github.com/diffplug/spotless/issues/2172)) +### Added +* Add option `manageTrailingCommas` to `ktfmt`. ([#2177](https://github.com/diffplug/spotless/pull/2177)) + +## [7.0.0.BETA1] - 2024-06-04 +### Added +* Full, no-asterisk support of Gradle configuration cache. ([#1274](https://github.com/diffplug/spotless/issues/1274), giving up on [#987](https://github.com/diffplug/spotless/issues/987)) + * In order to use `custom`, you must now use Gradle 8.0+. +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) +* Add support for formatting and sorting Maven POMs ([#2082](https://github.com/diffplug/spotless/issues/2082)) +### Fixed +* Check if ktlint_code_style is set in .editorconfig before overriding it ([#2143](https://github.com/diffplug/spotless/issues/2143)) +* Full no-asterisk support for configuration cache ([#2088](https://github.com/diffplug/spotless/pull/2088) closes [#1274](https://github.com/diffplug/spotless/issues/1274) and [#987](https://github.com/diffplug/spotless/issues/987)). +* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990)) +* Correctly provide EditorConfig property types for Ktlint ([#2052](https://github.com/diffplug/spotless/issues/2052)) +* Fixed memory leak introduced in 6.21.0 ([#2067](https://github.com/diffplug/spotless/issues/2067)) +* Made ShadowCopy (`npmInstallCache`) more robust by re-creating the cache dir if it goes missing ([#1984](https://github.com/diffplug/spotless/issues/1984),[2096](https://github.com/diffplug/spotless/pull/2096)) +* scalafmt.conf fileOverride section now works correctly ([#1854](https://github.com/diffplug/spotless/pull/1854)) +* Fix stdin pipe is being closed exception on Windows for large .proto files ([#2147](https://github.com/diffplug/spotless/issues/2147)) +* Reworked ShadowCopy (`npmInstallCache`) to use atomic filesystem operations, resolving several race conditions that could arise ([#2151](https://github.com/diffplug/spotless/pull/2151)) +### Changed +* Bump default `cleanthat` version to latest `2.16` -> `2.20`. ([#1725](https://github.com/diffplug/spotless/pull/1725)) +* Bump default `gherkin-utils` version to latest `8.0.2` -> `9.0.0`. ([#1703](https://github.com/diffplug/spotless/pull/1703)) +* Bump default `google-java-format` version to latest `1.19.2` -> `1.22.0`. ([#2129](https://github.com/diffplug/spotless/pull/2129)) +* Bump default `jackson` version to latest `2.14.2` -> `2.17.1`. ([#1685](https://github.com/diffplug/spotless/pull/1685)) +* Bump default `ktfmt` version to latest `0.46` -> `0.49`. ([#2045](https://github.com/diffplug/spotless/pull/2045), [#2127](https://github.com/diffplug/spotless/pull/2127)) +* Bump default `ktlint` version to latest `1.1.1` -> `1.2.1`. ([#2057](https://github.com/diffplug/spotless/pull/2057)) +* Bump default `scalafmt` version to latest `3.7.3` -> `3.8.1`. ([#1730](https://github.com/diffplug/spotless/pull/1730)) +* Bump default `shfmt` version to latest `3.7.0` -> `3.8.0`. ([#2050](https://github.com/diffplug/spotless/pull/2050)) +* Bump default `sortpom` version to latest `3.2.1` -> `4.0.0`. ([#2049](https://github.com/diffplug/spotless/pull/2049), [#2078](https://github.com/diffplug/spotless/pull/2078), [#2115](https://github.com/diffplug/spotless/pull/2115)) +* Bump default `zjsonpatch` version to latest `0.4.14` -> `0.4.16`. ([#1969](https://github.com/diffplug/spotless/pull/1969)) +* Bump default `jackson` version to latest `2.17.1` -> `2.17.2`. ([#2195](https://github.com/diffplug/spotless/pull/2195)) +### Removed +* **BREAKING** Fully removed `Rome`, use `Biome` instead. ([#2119](https://github.com/diffplug/spotless/pull/2119)) + +## [6.25.0] - 2024-01-23 +### Added +* Maven / Gradle - Support for formatting Java Docs for the Palantir formatter ([#2009](https://github.com/diffplug/spotless/pull/2009)) +* Support for `gofmt` ([#2001](https://github.com/diffplug/spotless/pull/2001)) + +## [6.24.0] - 2024-01-15 +### Added +* Support for shell formatting via [shfmt](https://github.com/mvdan/sh). ([#1994](https://github.com/diffplug/spotless/pull/1994)) +### Fixed +* Fix empty files with biome >= 1.5.0 when formatting files that are in the ignore list of the biome configuration file. ([#1989](https://github.com/diffplug/spotless/pull/1989) fixes [#1987](https://github.com/diffplug/spotless/issues/1987)) +* Fix a regression in BufStep where the same arguments were being provided to every `buf` invocation. ([#1976](https://github.com/diffplug/spotless/issues/1976)) +### Changed +* Use palantir-java-format 2.39.0 on Java 21. ([#1948](https://github.com/diffplug/spotless/pull/1948)) +* Bump default `ktlint` version to latest `1.0.1` -> `1.1.1`. ([#1973](https://github.com/diffplug/spotless/pull/1973)) +* Bump default `googleJavaFormat` version to latest `1.18.1` -> `1.19.2`. ([#1971](https://github.com/diffplug/spotless/pull/1971)) +* Bump default `diktat` version to latest `1.2.5` -> `2.0.0`. ([#1972](https://github.com/diffplug/spotless/pull/1972)) + +## [6.23.3] - 2023-12-04 +**BREAKING CHANGE** `6.23.0` made breaking changes to the ABI of the `KotlinExtension` and `GroovyExtension`. Those are reflected retroactively now. + - Previously, we had done semver on the Gradle plugin based only on buildscript compatibility. + - From now on, we will consider ABI for the benefit of convention-based plugins. +### Fixed +* Eclipse-based steps which contained any jars with a `+` in their path were broken, now fixed. ([#1860](https://github.com/diffplug/spotless/issues/1860#issuecomment-1826113332)) +* Make `KtfmtConfig.ConfigurableStyle#configure` public. ([#1926](https://github.com/diffplug/spotless/pull/1926)) +### Changed +* Bump default `palantir-java-format` version to latest `2.28.0` -> `2.38.0` on Java 21. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `googleJavaFormat` version to latest `1.17.0` -> `1.18.1`. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `ktfmt` version to latest `0.44` -> `0.46`. ([#1927](https://github.com/diffplug/spotless/pull/1927)) +* Bump default `eclipse` version to latest `4.27` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `greclipse` version to latest `4.28` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `cdt` version to latest `11.1` -> `11.3`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) + +## [6.23.2] - 2023-12-01 +### Fixed +* Fix a stuck MavenCentral sync from `6.23.1`. + +## [6.23.1] - 2023-11-29 +### Fixed +* Make `BaseGroovyExtension` and `BaseKotlinExtension` public. ([#1912](https://github.com/diffplug/spotless/pull/1912)) + +## [6.23.0] - 2023-11-27 +### Added +* Support custom rule sets for Ktlint. ([#1896](https://github.com/diffplug/spotless/pull/1896)) +### Fixed +* Fix `GoogleJavaFormatConfig.reorderImports` not working. ([#1872](https://github.com/diffplug/spotless/issues/1872)) +* Fix Eclipse JDT on some settings files. ([#1864](https://github.com/diffplug/spotless/pull/1864) fixes [#1638](https://github.com/diffplug/spotless/issues/1638)) +* Check if EditorConfig file exist for Ktlint in KotlinGradleExtension. ([#1889](https://github.com/diffplug/spotless/pull/1889)) +### Changed +* Bump default `ktlint` version to latest `1.0.0` -> `1.0.1`. ([#1855](https://github.com/diffplug/spotless/pull/1855)) +* Add a Step to remove semicolons from Groovy files. ([#1881](https://github.com/diffplug/spotless/pull/1881)) +* **POSSIBLY BREAKING** `userData` method has been removed from Ktlint step in ([#1891](https://github.com/diffplug/spotless/pull/1891)), you may use `editorConfigOverride` instead. +* **POSSIBLY BREAKING** Reuse configs for `KotlinExtension` and `KotlinGradleExtension` in ([#1890](https://github.com/diffplug/spotless/pull/1890)), this may break your integrations in Gradle Kotlin DSL, wait for Spotless 6.23.1 to fix this. +* **POSSIBLY BREAKING** Reuse configs for `GroovyExtension` and `GroovyGradleExtension` in ([#1892](https://github.com/diffplug/spotless/pull/1892)), this may break your integrations in Gradle Kotlin DSL, wait for Spotless 6.23.1 to fix this. + +## [6.22.0] - 2023-09-28 +### Added +* Added support for `google-java-format`'s `skip-javadoc-formatting` option ([#1793](https://github.com/diffplug/spotless/pull/1793)) +* Add support for `flexmark` in gradle. Previously only Maven was supported. ([#1801](https://github.com/diffplug/spotless/pull/1801)) +* Add support for biome. The Rome project [was renamed to Biome](https://biomejs.dev/blog/annoucing-biome/). + The configuration is still the same, but you should switch to the new `biome(...)` function and adjust + the version accordingly. ([#1804](https://github.com/diffplug/spotless/issues/1804)). +### Fixed +* Fixed support for plugins when using Prettier version `3.0.0` and newer. ([#1802](https://github.com/diffplug/spotless/pull/1802)) +### Changed +* Bump default `ktlint` version to latest `0.50.0` -> `1.0.0`. ([#1808](https://github.com/diffplug/spotless/pull/1808)) +* **POSSIBLY BREAKING** the default line endings are now `GIT_ATTRIBUTES_FAST_ALLSAME` instead of `GIT_ATTRIBUTES`. ([#1838](https://github.com/diffplug/spotless/pull/1838)) + * If all the files within a format have the same line endings, then there is no change in behavior. + * Fixes large performance regression. ([#1527](https://github.com/diffplug/spotless/issues/1527)) + +## [6.21.0] - 2023-08-29 +### Added +* Add a `jsonPatch` step to `json` formatter configurations. This allows patching of JSON documents using [JSON Patches](https://jsonpatch.com). ([#1753](https://github.com/diffplug/spotless/pull/1753)) +* Support GJF own import order. ([#1780](https://github.com/diffplug/spotless/pull/1780)) +### Fixed +* Add support for `prettier` version `3.0.0` and newer. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1751](https://github.com/diffplug/spotless/issues/1751)) +* Fix npm install calls when npm cache is not up-to-date. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1750](https://github.com/diffplug/spotless/issues/1750)) +* Fix configuration cache failure when using LineEnding.GIT_ATTRIBUTES ([#1644](https://github.com/diffplug/spotless/issues/1644)) +* Fix configuration cache failure when formatting proto files with Buf. ([#1779](https://github.com/diffplug/spotless/pull/1779)) +* Check if EditorConfig file exist for Ktlint. ([#1788](https://github.com/diffplug/spotless/pull/1788)) +### Changed +* Bump default `eslint` version to latest `8.31.0` -> `8.45.0` ([#1761](https://github.com/diffplug/spotless/pull/1761)) +* Bump default `prettier` version to latest (v2) `2.8.1` -> `2.8.8`. ([#1760](https://github.com/diffplug/spotless/pull/1760)) +* Bump default `greclipse` version to latest `4.27` -> `4.28`. ([#1775](https://github.com/diffplug/spotless/pull/1775)) + +## [6.20.0] - 2023-07-17 +### Added +* Add target option `targetExcludeIfContentContains` and `targetExcludeIfContentContainsRegex` to exclude files based on their text content. ([#1749](https://github.com/diffplug/spotless/pull/1749)) +* Add support for Protobuf formatting based on [Buf](https://buf.build/) ([#1208](https://github.com/diffplug/spotless/pull/1208)). +* Add an overload for `FormatExtension.addStep` which provides access to the `FormatExtension`'s `Provisioner`, enabling custom steps to make use of third-party dependencies. +### Fixed +* Correctly support the syntax + ``` + spotless { + yaml { + jackson().yamlFeature("MINIMIZE_QUOTES", true) + } + } + ``` +### Changed +* Bump default `cleanthat` version to latest `2.13` -> `2.17`. ([#1734](https://github.com/diffplug/spotless/pull/1734)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`. ([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. + * Dropped support for deprecated `useExperimental` parameter in favor of the `ktlint_experimental` property. + +## [6.19.0] - 2023-05-24 +### Added +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) +* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522)) +### Fixed +* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. ([#1666](https://github.com/diffplug/spotless/pull/1666)) +* Fixed a regression which changed the import sorting order in `googleJavaFormat` introduced in `6.18.0`. ([#1680](https://github.com/diffplug/spotless/pull/1680)) +* Equo-based formatters now work on platforms unsupported by Eclipse such as PowerPC (fixes [durian-swt#20](https://github.com/diffplug/durian-swt/issues/20)) +* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) +### Changed +* Equo-based formatters now download metadata to `~/.m2/repository/dev/equo/p2-data` rather than `~/.equo`, and for CI machines without a home directory the p2 data goes to `$GRADLE_USER_HOME/caches/p2-data`. ([#1714](https://github.com/diffplug/spotless/pull/1714)) +* Bump default `googleJavaFormat` version to latest `1.16.0` -> `1.17.0`. ([#1710](https://github.com/diffplug/spotless/pull/1710)) +* Bump default `ktfmt` version to latest `0.43` -> `0.44`. ([#1691](https://github.com/diffplug/spotless/pull/1691)) +* Bump default `ktlint` version to latest `0.48.2` -> `0.49.1`. ([#1696](https://github.com/diffplug/spotless/issues/1696)) + * Dropped support for `ktlint 0.46.x` following our policy of supporting two breaking changes at a time. +* Bump default `sortpom` version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) + +## [6.18.0] - 2023-04-06 +### Added +* `removeUnusedImport` can be configured to rely on `cleanthat-javaparser-unnecessaryimport`. Default remains `google-java-format`. ([#1589](https://github.com/diffplug/spotless/pull/1589)) +* Added formatter for Gherkin feature files ([#1649](https://github.com/diffplug/spotless/issues/1649)). +* Support configuration of mirrors for P2 repositories ([#1629](https://github.com/diffplug/spotless/issues/1629)): + ``` + spotless { + java { + eclipse().withP2Mirrors(['https://download.eclipse.org/': 'https://some.internal.mirror/eclipse']) + } + } + ``` + Mirrors are selected by prefix match, for example `https://download.eclipse.org/eclipse/updates/4.26/` will be redirected to `https://some.internal.mirror/eclipse/eclipse/updates/4.26/`. + The same configuration exists for `greclipse` and `eclipseCdt`. +* The `style` option in Palantir Java Format ([#1654](https://github.com/diffplug/spotless/pull/1654)). +### Fixed +* Stop using deprecated conventions when used in Gradle >= `7.1`. ([#1618](https://github.com/diffplug/spotless/pull/1618)) +### Changed +* **POTENTIALLY BREAKING** Drop support for `googleJavaFormat` versions < `1.8`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `cleanthat` version to latest `2.6` -> `2.13`. ([#1589](https://github.com/diffplug/spotless/pull/1589) and [#1661](https://github.com/diffplug/spotless/pull/1661)) +* Bump default `diktat` version `1.2.4.2` -> `1.2.5`. ([#1631](https://github.com/diffplug/spotless/pull/1631)) +* Bump default `flexmark` version `0.62.2` -> `0.64.0`. ([#1302](https://github.com/diffplug/spotless/pull/1302)) +* Bump default `googleJavaFormat` version `1.15.0` -> `1.16.0`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `scalafmt` version `3.7.1` -> `3.7.3`. ([#1584](https://github.com/diffplug/spotless/pull/1584)) +* Bump default Eclipse formatters for the 2023-03 release. ([#1662](https://github.com/diffplug/spotless/pull/1662)) + * JDT and GrEclipse `4.26` -> `4.27` + * Improve GrEclipse error reporting. ([#1660](https://github.com/diffplug/spotless/pull/1660)) + * CDT `11.0` -> `11.1` + +## [6.17.0] - 2023-03-13 +### Added +* You can now put the filename into a license header template with `$FILE`. ([#1605](https://github.com/diffplug/spotless/pull/1605) fixes [#1147](https://github.com/diffplug/spotless/issues/1147)) +* `licenseHeader` default pattern for Java files is updated to `(package|import|public|class|module) `. ([#1614](https://github.com/diffplug/spotless/pull/1614)) +### Changed +* All Eclipse formatters are now based on [Equo Solstice OSGi and p2 shim](https://github.com/equodev/equo-ide/tree/main/solstice). ([#1524](https://github.com/diffplug/spotless/pull/1524)) + * Eclipse JDT bumped default to `4.26` from `4.21`, oldest supported is `4.9`. + * We now recommend dropping the last `.0`, e.g. `4.26` instead of `4.26.0`, you'll get warnings to help you switch. + * Eclipse Groovy bumped default to `4.26` from `4.21`, oldest supported is `4.18`. + * Eclipse CDT bumped default to `11.0` from `4.21`, oldest supported is `10.6`. + * Eclipse WTP is still WIP at [#1622](https://github.com/diffplug/spotless/pull/1622). + +## [6.16.0] - 2023-02-27 +### Added +* `cleanthat` now has `includeDraft` option, to include draft mutators from composite mutators. ([#1574](https://github.com/diffplug/spotless/pull/1574)) +* `npm`-based formatters (`prettier`, `tsfmt` and `eslint`) now support caching of `node_modules` directory. + To enable it, provide `npmInstallCache()` option. ([#1590](https://github.com/diffplug/spotless/pull/1590)) +### Fixed +* `json { jackson()` can now handle `Array` as a root element. ([#1585](https://github.com/diffplug/spotless/pull/1585)) +* Reduce logging-noise created by `npm`-based formatters ([#1590](https://github.com/diffplug/spotless/pull/1590) fixes [#1582](https://github.com/diffplug/spotless/issues/1582)) +### Changed +* Bump default `cleanthat` version to latest `2.1` -> `2.6`. ([#1569](https://github.com/diffplug/spotless/pull/1569) and [#1574](https://github.com/diffplug/spotless/pull/1574)) + +## [6.15.0] - 2023-02-10 +### Added +* CleanThat Java Refactorer. ([#1560](https://github.com/diffplug/spotless/pull/1560)) +### Fixed +* Allow multiple instances of the same npm-based formatter to be used simultaneously. E.g. use prettier for typescript + *and* Java (using the community prettier-plugin-java) without messing up their respective `node_module` dependencies. ([#1565](https://github.com/diffplug/spotless/pull/1565)) +* `ktfmt` default style uses correct continuation indent. ([#1562](https://github.com/diffplug/spotless/pull/1562)) +### Changed +* Bump default `ktfmt` version to latest `0.42` -> `0.43` ([#1561](https://github.com/diffplug/spotless/pull/1561)) +* Bump default `jackson` version to latest `2.14.1` -> `2.14.2` ([#1536](https://github.com/diffplug/spotless/pull/1536)) + +## [6.14.1] - 2023-02-05 +### Fixed +* `freshmark` fixed on java 15+ ([#1304](https://github.com/diffplug/spotless/pull/1304) fixes [#803](https://github.com/diffplug/spotless/issues/803)) +* **POTENTIALLY BREAKING** `sortByKeys` for JSON formatting now takes into account objects inside arrays ([#1546](https://github.com/diffplug/spotless/pull/1546)) + +## [6.14.0] - 2023-01-26 +### Added +* Support `jackson()` for YAML and JSON files ([#1492](https://github.com/diffplug/spotless/pull/1492)) +* Prettier will now suggest to install plugins if a parser cannot be inferred from the file extension ([#1511](https://github.com/diffplug/spotless/pull/1511)) +* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500)) +### Fixed +* **POTENTIALLY BREAKING** Generate the correct qualifiedRuleId for Ktlint 0.48.x [#1495](https://github.com/diffplug/spotless/pull/1495) +* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494) +### Changed +* **POTENTIALLY BREAKING** Bump minimum JRE from 8 to 11 ([#1514](https://github.com/diffplug/spotless/pull/1514) part 1 of [#1337](https://github.com/diffplug/spotless/issues/1337)) + * You can bump your build JRE without bumping your requirements ([docs](https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation)). +* Prettier will now suggest to install plugins if a parser cannot be inferred from the file extension ([#1511](https://github.com/diffplug/spotless/pull/1511)) +* **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) + * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. + * From now on, we will support no more than 2 breaking changes at a time. +* `npm`-based formatters `ESLint`, `prettier` and `tsfmt` delay their `npm install` call until the formatters are first + used. For Gradle this effectively moves the `npm install` call out of the configuration phase and as such enables + better integration with `gradle-node-plugin`. ([#1522](https://github.com/diffplug/spotless/pull/1522)) +* Bump default `ktlint` version to latest `0.48.1` -> `0.48.2` ([#1529](https://github.com/diffplug/spotless/pull/1529)) +* Bump default `scalafmt` version to latest `3.6.1` -> `3.7.1` ([#1529](https://github.com/diffplug/spotless/pull/1529)) + +## [6.13.0] - 2023-01-14 +### Added +* **POTENTIALLY BREAKING** `ktlint` step now supports `.editorconfig` ([#1442](https://github.com/diffplug/spotless/pull/1442) implements [#142](https://github.com/diffplug/spotless/issues/142)) + * **POTENTIALLY BREAKING** `ktlint` step now modifies license headers. Make sure to put `licenseHeader` *after* `ktlint`. +* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441)) +* Added support for npm-based [ESLint](https://eslint.org/) formatter for javascript and typescript ([#1453](https://github.com/diffplug/spotless/pull/1453)) +* Better suggested messages when user's default is set by JVM limitation. ([#995](https://github.com/diffplug/spotless/pull/995)) +### Fixed +* Prevent tool configurations from being resolved outside project ([#1447](https://github.com/diffplug/spotless/pull/1447) fixes [#1215](https://github.com/diffplug/spotless/issues/1215)) +* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444)) +* Fix subgroups leading catch all matcher. +### Changed +* Bump default `ktlint` version to latest `0.47.1` -> `0.48.1` ([#1456](https://github.com/diffplug/spotless/pull/1456)) +* Bump default version for `prettier` from `2.0.5` to `2.8.1` ([#1453](https://github.com/diffplug/spotless/pull/1453)) + +## [6.12.1] - 2023-01-02 +### Fixed +* Improve memory usage when using git ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426)) +* Support `ktlint` 0.48+ ([#1432](https://github.com/diffplug/spotless/pull/1432)) fixes ([#1430](https://github.com/diffplug/spotless/issues/1430)) +### Changed +* Bump default `ktlint` version to latest `0.47.1` -> `0.48.0` ([#1432](https://github.com/diffplug/spotless/pull/1432)) +* Bump default `ktfmt` version to latest `0.41` -> `0.42` ([#1421](https://github.com/diffplug/spotless/pull/1421)) + +## [6.12.0] - 2022-11-24 +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) +### Fixed +* Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) +* Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) +### Changed +* Bump default `ktfmt` version to latest `0.40` -> `0.41` ([#1340](https://github.com/diffplug/spotless/pull/1340)) +* Bump default `scalafmt` version to latest `3.5.9` -> `3.6.1` ([#1373](https://github.com/diffplug/spotless/pull/1373)) +* Bump default `diktat` version to latest `1.2.3` -> `1.2.4.2` ([#1393](https://github.com/diffplug/spotless/pull/1393)) +* Bump default `palantir-java-format` version to latest `2.10` -> `2.28` ([#1393](https://github.com/diffplug/spotless/pull/1393)) + +## [6.11.0] - 2022-09-14 +### Added +* `formatAnnotations()` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat()`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) +### Changed +* Bump default `ktfmt` version to latest `0.39` -> `0.40` ([#1312](https://github.com/diffplug/spotless/pull/1312)) +* Bump default `ktlint` version to latest `0.46.1` -> `0.47.1` ([#1303](https://github.com/diffplug/spotless/pull/1303)) + * Also restored support for older versions of ktlint back to `0.31.0` + +## [6.10.0] - 2022-08-23 +### Added +* `scalafmt` integration now has a configuration option `majorScalaVersion` that allows you to configure the Scala version that gets resolved from the Maven artifact ([#1283](https://github.com/diffplug/spotless/pull/1283)) +### Changed +* Add the `ktlint` rule in error messages when `ktlint` fails to apply a fix ([#1279](https://github.com/diffplug/spotless/pull/1279)) +* Bump default `scalafmt` to latest `3.0.8` -> `3.5.9` (removed support for pre-`3.0.0`) ([#1283](https://github.com/diffplug/spotless/pull/1283)) + +## [6.9.1] - 2022-08-10 +### Fixed +* Fix Clang not knowing the filename and changing the format ([#1268](https://github.com/diffplug/spotless/pull/1268) fixes [#1267](https://github.com/diffplug/spotless/issues/1267)). +### Changed +* Bump default `diktat` version to latest `1.2.1` -> `1.2.3` ([#1266](https://github.com/diffplug/spotless/pull/1266)) + +## [6.9.0] - 2022-07-28 +### Added +* Clang and Black no longer break the build when the binary is unavailable, if they will not be run during that build ([#1257](https://github.com/diffplug/spotless/pull/1257)). +* License header support for Kotlin files without `package` or `@file` but do at least have `import` ([#1263](https://github.com/diffplug/spotless/pull/1263)). +### Fixed +* Warnings about missing `Task#usesService` for Gradle 8.0 ([#1262](https://github.com/diffplug/spotless/pull/1262) fixes [#1260](https://github.com/diffplug/spotless/issues/1260)) + +## [6.8.0] - 2022-06-30 +### Added +* Support for `MAC_CLASSIC` (`\r`) line ending ([#1243](https://github.com/diffplug/spotless/pull/1243) fixes [#1196](https://github.com/diffplug/spotless/issues/1196)) +### Changed +* Bump default `ktlint` version to latest `0.45.2` -> `0.46.1` ([#1239](https://github.com/diffplug/spotless/issues/1239)) + * Minimum supported version also bumped to `0.46.0` (we have abandoned strong backward compatibility for `ktlint`, from here on out Spotless will only support the most-recent breaking change). +* Bump default `diktat` version to latest `1.1.0` -> `1.2.1` ([#1246](https://github.com/diffplug/spotless/pull/1246)) + * Minimum supported version also bumped to `1.2.1` (diktat is based on ktlint and has the same backward compatibility issues). +* Bump default `ktfmt` version to latest `0.37` -> `0.39` ([#1240](https://github.com/diffplug/spotless/pull/1240)) + +## [6.7.2] - 2022-06-11 +### Fixed +* `PalantirJavaFormatStep` no longer needs the `--add-exports` calls in the `org.gradle.jvmargs` property in `gradle.properties`. ([#1233](https://github.com/diffplug/spotless/pull/1233)) + +## [6.7.1] - 2022-06-10 +### Fixed +* (Second try) `googleJavaFormat` and `removeUnusedImports` works on JDK16+ without jvm args workaround. ([#1228](https://github.com/diffplug/spotless/pull/1228)) + * If you have a bunch of `--add-exports` calls in your `org.gradle.jvmargs` property in `gradle.properties`, you should be able to remove them. (fixes [#834](https://github.com/diffplug/spotless/issues/834#issuecomment-819118761)) + +## [6.7.0] - 2022-06-05 +### Added +* Support for `editorConfigOverride` in `ktlint`. ([#1218](https://github.com/diffplug/spotless/pull/1218) fixes [#1193](https://github.com/diffplug/spotless/issues/1193)) + * If you are using properties like `indent_size`, you should pass now pass them as `editorConfigOverride` and not as `userData`. +### Fixed +* `googleJavaFormat` and `removeUnusedImports` works on JDK16+ without jvm args workaround. ([#1224](https://github.com/diffplug/spotless/pull/1224)) + * If you have a bunch of `--add-exports` calls in your `org.gradle.jvmargs` property in `gradle.properties`, you should be able to remove them. (fixes [#834](https://github.com/diffplug/spotless/issues/834#issuecomment-819118761)) + +## [6.6.1] - 2022-05-13 +### Fixed +* More daemon memory consumption fixes ([#1206](https://github.com/diffplug/spotless/pull/1198) fixes [#1194](https://github.com/diffplug/spotless/issues/1194)) + +## [6.6.0] - 2022-05-10 +### Added +* `FormatExtension.createIndependentApplyTaskLazy`, with same functionality as `createIndependentApplyTaskLazy` but returning `TaskProvider` ([#1198](https://github.com/diffplug/spotless/pull/1198)) +### Fixed +* Update the `black` version regex to fix `19.10b0` and earlier. (fixes [#1195](https://github.com/diffplug/spotless/issues/1195), regression introduced in `6.5.0`) +* Improved daemon memory consumption ([#1198](https://github.com/diffplug/spotless/pull/1198) fixes [#1194](https://github.com/diffplug/spotless/issues/1194)) +### Changed +* Bump default `ktfmt` version to latest `0.36` -> `0.37`. ([#1200](https://github.com/diffplug/spotless/pull/1200)) + +## [6.5.2] - 2022-05-03 +### Changed +* Bump default `diktat` version to latest `1.0.1` -> `1.1.0`. ([#1190](https://github.com/diffplug/spotless/pull/1190)) + * Converted `diktat` integration to use a compile-only source set. (fixes [#524](https://github.com/diffplug/spotless/issues/524)) + * Use the full path to a file in `diktat` integration. (fixes [#1189](https://github.com/diffplug/spotless/issues/1189)) + +## [6.5.1] - 2022-04-27 +### Changed +* Bump default `ktfmt` version to latest `0.35` -> `0.36`. ([#1183](https://github.com/diffplug/spotless/issues/1183)) +* Bump default `google-java-format` version to latest `1.13.0` -> `1.15.0`. + * ~~This means it is no longer necessary to use the `--add-exports` workaround (fixes [#834](https://github.com/diffplug/spotless/issues/834)).~~ `--add-exports` workaround is still needed. + +## [6.5.0] - 2022-04-22 +### Added +* Added a `runToFixMessage` property to customize the run-to-fix message in `spotlessCheck` task. ([#1175](https://github.com/diffplug/spotless/issues/1175)) +* Added support for enabling ktlint experimental ruleset. ([#1145](https://github.com/diffplug/spotless/pull/1168)) +### Fixed +* Fixed support for Python Black's new version reporting. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* All tasks (including helper tasks) are now part of the `verification` group. (fixes [#1050](https://github.com/diffplug/spotless/issues/1050)) +* Error messages for unexpected file encoding now works on Java 8. (fixes [#1081](https://github.com/diffplug/spotless/issues/1081)) +### Changed +* Spotless now applies the `base` plugin to make sure that Spotless always has a `check` task to hook into. ([#1179](https://github.com/diffplug/spotless/pull/1179), fixes [#1164](https://github.com/diffplug/spotless/pull/1164), reverts [#1014](https://github.com/diffplug/spotless/pull/1014)) + * Spotless used to work this way, we stopped applying base starting with version [`6.0.3` (released Dec 2021)](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#603---2021-12-06) in order to play nicely with a now-outdated Android template, but not applying `base` causes more problems than it fixes (see [#1164](https://github.com/diffplug/spotless/pull/1164) for a good example). + * If you have anything like `tasks.register("clean"` or `tasks.register("clean", Delete)`, just change the `register` to `named` so that you are configuring the existing `clean` created by `base`, rather than creating a new task. +* Bump default `black` version to latest `19.10b0` -> `22.3.0`. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* Bump default `ktfmt` version to latest `0.34` -> `0.35`. ([#1159](https://github.com/diffplug/spotless/pull/1159)) +* Bump default `ktlint` version to latest `0.43.2` -> `0.45.2`. ([#1177](https://github.com/diffplug/spotless/pull/1177)) + +## [6.4.2] - 2022-04-06 +### Fixed +* Git user config and system config also included for defaultEndings configuration. ([#540](https://github.com/diffplug/spotless/issues/540)) + +## [6.4.1] - 2022-03-30 +### Fixed +* Fixed ktfmt options configuration in Gradle plugin for Gradle Kotlin scripts (kts). + +## [6.4.0] - 2022-03-28 +### Added +* Accept `java.nio.charset.Charset` type when setting the character encoding via `encoding` ([#1128](https://github.com/diffplug/spotless/issues/1128)) +* Added support for setting custom parameters for Kotlin ktfmt in Gradle plugin. ([#1145](https://github.com/diffplug/spotless/pull/1145)) + +## [6.3.0] - 2022-02-15 +### Added +* Added support for JSON formatting based on [Gson](https://github.com/google/gson) ([#1125](https://github.com/diffplug/spotless/pull/1125)). +### Changed +* Use SLF4J for logging ([#1116](https://github.com/diffplug/spotless/issues/1116)) + +## [6.2.2] - 2022-02-09 +### Changed +* Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). + +## [6.2.1] - 2022-02-01 ### Changed * Bump default versions of formatters ([#1095](https://github.com/diffplug/spotless/pull/1095)). * google-java-format `1.12.0` -> `1.13.0` * ktfmt `0.29` -> `0.30` +* Added support for git property `core.autocrlf` ([#540](https://github.com/diffplug/spotless/issues/540)) ## [6.2.0] - 2022-01-13 ### Added @@ -50,7 +532,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [6.0.4] - 2021-12-07 ### Fixed -* Fix gradle composite builds ([#860](https://github.com/diffplug/spotless/issues/860)). +* Fix Gradle composite builds ([#860](https://github.com/diffplug/spotless/issues/860)). ## [6.0.3] - 2021-12-06 ### Fixed @@ -175,7 +657,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * Update ktfmt from 0.21 to 0.24 ### Fixed -* The `` field in the maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) +* The `` field in the Maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) * Node is re-installed if some other build step removed it ([#863](https://github.com/diffplug/spotless/issues/863)) ## [5.12.4] - 2021-04-21 @@ -366,7 +848,7 @@ println "isEager $isEager" * LineEndings.GIT_ATTRIBUTES is now a bit more efficient, and paves the way for remote build cache support in Gradle. ([#621](https://github.com/diffplug/spotless/pull/621)) * `ratchetFrom` now ratchets from the merge base of `HEAD` and the specified branch. This fixes the surprising behavior when a remote branch advanced ([#631](https://github.com/diffplug/spotless/pull/631) fixes [#627](https://github.com/diffplug/spotless/issues/627)). ### Deprecated -* The default targets for `C/C++`, `freshmark`, `sql`, and `typescript` now generate a warning, asking the user to specify a target manually. There is no well-established convention for these languages in the gradle ecosystem, and the performance of the default target is far worse than a user-provided one. If you dislike this change, please complain in [#634](https://github.com/diffplug/spotless/pull/634). +* The default targets for `C/C++`, `freshmark`, `sql`, and `typescript` now generate a warning, asking the user to specify a target manually. There is no well-established convention for these languages in the Gradle ecosystem, and the performance of the default target is far worse than a user-provided one. If you dislike this change, please complain in [#634](https://github.com/diffplug/spotless/pull/634). * `customLazy` and `customLazyGroovy` now generate a warning, asking the user to migrate to `custom`. There is no longer a performance advantage to `customLazy` in the new modern plugin. See [#635](https://github.com/diffplug/spotless/pull/635/files) for example migrations. * inside the `cpp { }` block, the `eclipse` step now generates a warning, asking you to switch to `eclipseCdt`. It is the same underlying step, but the new name clears up any confusion with the more common Java `eclipse`. [#636](https://github.com/diffplug/spotless/pull/635/files) @@ -444,7 +926,7 @@ spotless { **TLDR: This version improves performance and adds support for the local Gradle Build Cache. You will not need to make any changes in your buildscript.** It is a breaking change only for a few users who have built *other* plugins on top of this one. ### Added -* Support for the gradle build cache. ([#576](https://github.com/diffplug/spotless/pull/576)) +* Support for the Gradle build cache. ([#576](https://github.com/diffplug/spotless/pull/576)) * The local cache will work great, but the remote cache will always miss until [#566](https://github.com/diffplug/spotless/issues/566) is resolved. ### Removed * **BREAKING** it used to be possible for any project to format files in any other project. For example, `:a` could format files in `:b`. It is now only possible to format files within the project directory. It is okay (but not advised) to format files in subprojects, since they are within the project directory. @@ -452,11 +934,11 @@ spotless { * Previously, the `check` and `apply` tasks were just marker tasks, and they called `setCheck` and `setApply` on the "worker" task. Now `check` and `apply` are real tasks in their own right, so the marker-task kludge is no longer necessary. ### Changed * (Power users only) **BREAKING** `SpotlessTask FormatExtension::createIndependentTask` has been removed, and replaced with `SpotlessApply::createIndependentApplyTask`. ([#576](https://github.com/diffplug/spotless/pull/576)) -* Improve suggested gradle invocation for running `spotlessApply`. ([#578](https://github.com/diffplug/spotless/pull/578)) +* Improve suggested Gradle invocation for running `spotlessApply`. ([#578](https://github.com/diffplug/spotless/pull/578)) ## [3.30.0] - 2020-05-11 ### Added -* `-PspotlessIdeHook` which makes the VS Code extension faster and more reliable. See [`IDE_INTEGRATION.md`](IDE_INTEGRATION.md) for more details. ([#568](https://github.com/diffplug/spotless/pull/568)) +* `-PspotlessIdeHook` which makes the VS Code extension faster and more reliable. See [`IDE_HOOK.md`](IDE_HOOK.md) for more details. ([#568](https://github.com/diffplug/spotless/pull/568)) ## [3.29.0] - 2020-05-05 ### Added @@ -543,11 +1025,11 @@ spotless { * Updated default eclipse-jdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * Updated default eclipse-cdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * **KNOWN BUG - accidentally published CDT 9.7 rather than 9.8 - fixed in 3.26.0** -* Added new maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) +* Added new Maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) ## [3.23.1] - 2019-06-17 * Fixes incorrect M2 cache directory path handling of Eclipse based formatters ([#401](https://github.com/diffplug/spotless/issues/401)) -* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) +* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because Gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) ## [3.23.0] - 2019-04-24 * Updated default ktlint from 0.21.0 to 0.32.0, and Maven coords to com.pinterest ([#394](https://github.com/diffplug/spotless/pull/394)) @@ -677,7 +1159,7 @@ spotless { ## [3.4.0] - 2017-05-21 * `ImportOrderStep` can now handle multi-line comments and misplaced imports. * Groovy extension now checks for the `groovy` plugin to be applied. -* Deprecated the old syntax for the the eclipse formatter: +* Deprecated the old syntax for the eclipse formatter: + New syntax better separates the version from the other configuration options, and is more consistent with the other + `eclipseFormatFile('format.xml')` -> `eclipse().configFile('format.xml')` + `eclipseFormatFile('4.4.0', 'format.xml')` -> `eclipse('4.4.0').configFile('format.xml')` @@ -707,7 +1189,7 @@ spotless { ## [3.0.0] - 2017-01-09 * BREAKING CHANGE: `customReplace` and `customReplaceRegex` renamed to just `replace` and `replaceRegex`. -* BREAKING CHANGE: Plugin portal ID is still `com.diffplug.gradle.spotless`, but maven coordinate has changed to `com.diffplug.spotless:spotless-plugin-gradle`. +* BREAKING CHANGE: Plugin portal ID is still `com.diffplug.gradle.spotless`, but Maven coordinate has changed to `com.diffplug.spotless:spotless-plugin-gradle`. * HUGE SPEEDUP: Now supports incremental build / up-to-date-checking. + If you are using `custom` or `customLazy`, you might want to take a look at [this javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.27.0/com/diffplug/gradle/spotless/FormatExtension.html#bumpThisNumberIfACustomStepChanges-int-). * BREAKING CHANGE: `freshmark` no longer includes all project properties by default. All properties must now be added manually: diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 9515bb0381..da51e31244 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -4,26 +4,20 @@ [![Gradle plugin](https://img.shields.io/badge/plugins.gradle.org-com.diffplug.spotless-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.spotless) -[![Maven central](https://img.shields.io/badge/mavencentral-yes-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-yes-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.0/index.html) -[![License Apache](https://img.shields.io/badge/license-apache-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) -[![Changelog](https://img.shields.io/badge/changelog-6.2.0-blue.svg)](CHANGES.md) +[![Changelog](https://img.shields.io/badge/changelog-7.0.3-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/7.0.3/index.html) -[![Circle CI](https://circleci.com/gh/diffplug/spotless/tree/main.svg?style=shield)](https://circleci.com/gh/diffplug/spotless/tree/main) -[![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless) [![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) [![Add other IDE](https://img.shields.io/badge/IDE-add_yours-blueviolet.svg)](IDE_HOOK.md) @@ -33,7 +27,7 @@ output = [ output = prefixDelimiterReplace(input, 'https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/', '/', versionLast) --> -Spotless is a general-purpose formatting plugin used by [4,000 projects on GitHub (August 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. +Spotless is a general-purpose formatting plugin used by [15,000 projects on GitHub (Jan 2023)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. To people who use your build, it looks like this ([IDE support also available](IDE_HOOK.md)): @@ -59,25 +53,31 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [**Quickstart**](#quickstart) - [Requirements](#requirements) + - [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)) + - [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)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) - [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt)) + - [Protobuf](#protobuf) ([buf](#buf), [clang-format](#clang-format)) - [Python](#python) ([black](#black)) - [FreshMark](#freshmark) aka markdown + - [Flexmark](#flexmark) aka markdown - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) - [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier)) - - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier)) - - [JSON](#json) + - [Maven POM](#maven-pom) ([sortPom](#sortpom)) + - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Biome](#biome)) + - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Biome](#biome)) + - [JSON](#json) ([simple](#simple), [gson](#gson), [jackson](#jackson), [Biome](#biome), [jsonPatch](#jsonPatch)) + - [YAML](#yaml) + - [Shell](#shell) + - [Gherkin](#gherkin) - Multiple languages - - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection)) - - javascript, jsx, angular, vue, flow, typescript, css, less, scss, html, json, graphql, markdown, ymaml + - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection), [caching `npm install` results](#caching-results-of-npm-install)) - [clang-format](#clang-format) - - c, c++, c#, objective-c, protobuf, javascript, java - [eclipse web tools platform](#eclipse-web-tools-platform) - - css, html, js, json, xml + - [Biome](#biome) ([binary detection](#biome-binary), [config file](#biome-configuration-file), [input language](#biome-input-language)) - **Language independent** - [Generic steps](#generic-steps) - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) @@ -105,18 +105,20 @@ spotless { format 'misc', { // define the files to apply `misc` to - target '*.gradle', '*.md', '.gitignore' + target '*.gradle', '.gitattributes', '.gitignore' // define the steps to apply to those files trimTrailingWhitespace() - indentWithTabs() // or spaces. Takes an integer argument if you don't like 4 + leadingSpacesToTabs() // or leadingTabsToSpaces. Takes an integer argument if you don't like 4 endWithNewline() } java { // don't need to set target, it is inferred from java // apply a specific flavor of google-java-format - googleJavaFormat('1.8').aosp().reflowLongStrings() + googleJavaFormat('1.8').aosp().reflowLongStrings().skipJavadocFormatting() + // fix formatting of type annotations + formatAnnotations() // make sure every file has the following copyright header. // optionally, Spotless can set copyright years by digging // through git history (see "license" section below) @@ -126,22 +128,52 @@ 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/6.2.0/com/diffplug/gradle/spotless/FormatExtension.html#target-java.lang.Object...-) and [`targetExclude`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/6.2.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/6.2.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/6.2.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/6.2.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/6.2.0/com/diffplug/gradle/spotless/FormatExtension.html#prettier--), [`eclipseWtp`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/6.2.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/7.0.3/com/diffplug/gradle/spotless/FormatExtension.html#target-java.lang.Object...-) and [`targetExclude`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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/7.0.3/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/7.0.3/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/7.0.3/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/7.0.3/com/diffplug/gradle/spotless/FormatExtension.html#custom-java.lang.String-groovy.lang.Closure-), [`prettier`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/com/diffplug/gradle/spotless/FormatExtension.html#prettier--), [`eclipseWtp`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/com/diffplug/gradle/spotless/FormatExtension.html#eclipseWtp-com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep-), [`licenseHeader`](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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/6.2.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/7.0.3/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 -Spotless requires JRE 8+, and Gradle 6.1.1+. Some steps require JRE 11+, `Unsupported major.minor version` means you're using a step that needs a newer JRE. +Spotless requires JRE 11+ and Gradle 6.1.1 or newer. -If you're stuck on an older version of Gradle, `id 'com.diffplug.gradle.spotless' version '4.5.1'` supports all the way back to Gradle 2.x`. +- If you're stuck on JRE 8, use [`id 'com.diffplug.spotless' version '6.13.0'` or older](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#6130---2023-01-14). +- If you're stuck on an older version of Gradle, [`id 'com.diffplug.gradle.spotless' version '4.5.1'` supports all the way back to Gradle 2.x](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#451---2020-07-04). + +### Linting + +Starting in version `7.0.0`, Spotless now supports linting in addition to formatting. To Spotless, all lints are errors which must be either fixed or suppressed. Lints show up like this: + +```console +user@machine repo % ./gradlew build +:spotlessKotlinCheck FAILED + There were 2 lint error(s), they must be fixed or suppressed. + src/main/kotlin/com/diffplug/Foo.kt:L7 ktlint(standard:no-wildcard-imports) Wildcard import + src/main/kotlin/com/diffplug/Bar.kt:L9 ktlint(standard:no-wildcard-imports) Wildcard import + Resolve these lints or suppress with `suppressLintsFor` +``` + +To suppress lints, you can do this: + +```gradle +spotless { + kotlin { + ktlint() + suppressLintsFor { + step = 'ktlint' + shortCode = 'standard:no-wildcard-imports' + } + } +} +``` + +Spotless is primarily a formatter, _not_ a linter. In our opinion, a linter is just a broken formatter. But formatters do break sometimes, and representing these failures as lints that can be suppressed is more useful than just giving up. ## Java -`com.diffplug.gradle.spotless.JavaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -149,18 +181,24 @@ spotless { // Use the default importOrder configuration importOrder() // optional: you can specify import groups directly - // note: you can use an empty string for all the imports you didn't specify explicitly, and '\\#` prefix for static imports - importOrder('java', 'javax', 'com.acme', '', '\\#com.acme', '\\#') + // note: you can use an empty string for all the imports you didn't specify explicitly, '|' to join group without blank line, and '\\#` prefix for static imports + importOrder('java|javax', 'com.acme', '', '\\#com.acme', '\\#') // optional: instead of specifying import groups directly you can specify a config file // export config file: https://github.com/diffplug/spotless/blob/main/ECLIPSE_SCREENSHOTS.md#creating-spotlessimportorder importOrderFile('eclipse-import-order.txt') // import order file as exported from eclipse removeUnusedImports() - googleJavaFormat() // has its own section below - eclipse() // has its own section below - prettier() // has its own section below - clangFormat() // has its own section below + // Cleanthat will refactor your code, but it may break your style: apply it before your formatter + cleanthat() // has its own section below + + // Choose one of these formatters. + googleJavaFormat() // has its own section below + eclipse() // has its own section below + prettier() // has its own section below + clangFormat() // has its own section below + + formatAnnotations() // fixes formatting of type annotations, see below licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile } @@ -169,12 +207,24 @@ spotless { -The target is usually inferred automatically from the java source sets. However, Spotless cannot automatically detect [android](https://github.com/diffplug/spotless/issues/111) or [java-gradle-plugin](https://github.com/diffplug/spotless/issues/437) sources, but you can fix this easily: +> [!WARNING] +> The target is usually inferred automatically from the java source sets. However, Spotless cannot automatically detect [android](https://github.com/diffplug/spotless/issues/111) or [java-gradle-plugin](https://github.com/diffplug/spotless/issues/437) sources, but you can fix this easily: +> +> ```gradle +> spotless { +> java { +> target 'src/*/java/**/*.java' +> ``` -```gradle +### removeUnusedImports + +``` spotless { java { - target 'src/*/java/**/*.java' + removeUnusedImports() + // optional: you may switch for `google-java-format` as underlying engine to `cleanthat-javaparser-unnecessaryimport` + // which enables processing any language level source file with a JDK8+ Runtime + removeUnusedImports('cleanthat-javaparser-unnecessaryimport') ``` ### google-java-format @@ -184,63 +234,138 @@ spotless { spotless { java { googleJavaFormat() - // optional: you can specify a specific version and/or switch to AOSP style - // and/or reflow long strings (requires at least 1.8) + // optional: you can specify a specific version (>= 1.8) and/or switch to AOSP style + // and/or reflow long strings // and/or use custom group artifact (you probably don't need this) - googleJavaFormat('1.8').aosp().reflowLongStrings().groupArtifact('com.google.googlejavaformat:google-java-format') + googleJavaFormat('1.8').aosp().reflowLongStrings().formatJavadoc(false).reorderImports(false).groupArtifact('com.google.googlejavaformat:google-java-format') ``` -**âš ī¸ Note on using Google Java Format with Java 16+** - -Using Java 16+ with Google Java Format 1.10.0 [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) to the running JDK. -These Flags can be provided using the `gradle.properties` file (See [documentation](https://docs.gradle.org/current/userguide/build_environment.html)). +### palantir-java-format -For example the following file under `gradle.properties` will run maven with the required flags: +[homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). +```gradle +spotless { + java { + palantirJavaFormat() + // optional: you can specify a specific version and/or switch to AOSP/GOOGLE style + palantirJavaFormat('2.9.0').style("GOOGLE") + // optional: you can also format Javadocs, requires at least Palantir 2.39.0 + palantirJavaFormat('2.39.0').formatJavadoc(true) ``` -org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + +### 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. + +```gradle +spotless { + java { + eclipse() + // optional: you can specify a specific version and/or config file + eclipse('4.26').configFile('eclipse-prefs.xml') + // Or supply the configuration as a string + eclipse('4.26').configProperties(""" + ... + """) + // if the access to the p2 repositories is restricted, mirrors can be + // specified using a URI prefix map as follows: + eclipse().withP2Mirrors(['https://download.eclipse.org/eclipse/updates/4.29/':'https://some.internal.mirror/4-29-updates-p2/']) ``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). -### palantir-java-format +#### Sort Members + +Not only can you format your code with Eclipse JDT, but you can also sort the members as you know it from Eclipse IDE. +This ensures that the methods are always in sorted order (and thus reduces the likelihood of collisions in a version +control system). It is turned off by default, but you might want to consider enabling it when setting coding standards +for a project. + +The format to specify the sort order follows the `outlinesortoption` and `org.eclipse.jdt.ui.visibility.order` +properties that can be found in the workspace folder of your Eclipse IDE. You can look at the +file `.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs` in your workspace directory. -[homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). ```gradle spotless { java { - palantirJavaFormat() - // optional: you can specify a specific version - palantirJavaFormat('2.9.0') + eclipse() + // Optional: Enable the Sort Members feature globally. (default: false) + .sortMembersEnabled(true) + // Optional: Specify the sort order of the member categories. (default: T,SF,SI,SM,F,I,C,M) + // SF,SI,SM,F,I,C,M,T = Static Fields, Static Initializers, Static Methods, Fields, Initializers, Constructors, Methods, (Nested) Types + .sortMembersOrder("SF,SI,SM,F,I,C,M,T") + // Optional: Enable the reordering of fields, enum constants, and initializers. (default: true) + .sortMembersDoNotSortFields(false) + // Optional: Enable reordering of members of the same category by the visibility within the category. (default: false) + .sortMembersVisibilityOrderEnabled(true) + // Optional: Specify the ordering of members of the same category by the visibility within the category. (default: B,V,R,D) + // B,R,D,V = Public, Protected, Package, Private + .sortMembersVisibilityOrder("B,R,D,V") ``` -**âš ī¸ Note on using Palantir Java Format with Java 16+** +You can enable/disable the globally defined sort properties on file level by adding the following comments: +- `// @SortMembers:enabled=false` - disable the Sort Members feature for this file +- `// @SortMembers:doNotSortFields=true` - disable the sorting of static and instance fields +- `// @SortMembers:sortByVisibility=false` - don't sort members by its visibility modifier -Using Java 16+ with Palantir Java Format [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) on the running JDK. -These Flags can be provided using the `gradle.properties` file (See [documentation](https://docs.gradle.org/current/userguide/build_environment.html)). +### formatAnnotations -For example the following file under `gradle.properties` will run maven with the required flags: +Type annotations should be on the same line as the type that they qualify. + +```java + @Override + @Deprecated + @Nullable @Interned String s; ``` -org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + +However, some tools format them incorrectly, like this: + +```java + @Override + @Deprecated + @Nullable + @Interned + String s; ``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). -### eclipse jdt +To fix the incorrect formatting, add the `formatAnnotations()` rule after a Java formatter. For example: + +```gradle +spotless { + java { + googleJavaFormat() + formatAnnotations() + } +} +``` + +This does not re-order annotations, it just removes incorrect newlines. + +A type annotation is an annotation that is meta-annotated with `@Target({ElementType.TYPE_USE})`. +Spotless has a default list of well-known type annotations. +You can use `addTypeAnnotation()` and `removeTypeAnnotation()` to override its defaults: + +```gradle + formatAnnotations().addTypeAnnotation("Empty").addTypeAnnotation("NonEmpty").removeTypeAnnotation("Localized") +``` + +You can make a pull request to add new annotations to Spotless's default list. -[homepage](https://www.eclipse.org/downloads/packages/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter). See [here](../ECLIPSE_SCREENSHOTS.md) for screenshots that demonstrate how to get and install the config file mentioned below. +### cleanthat + +[homepage](https://github.com/solven-eu/cleanthat). CleanThat enables automatic refactoring of Java code. [ChangeLog](https://github.com/solven-eu/cleanthat/blob/master/CHANGES.MD) ```gradle spotless { java { - eclipse() + cleanthat() // optional: you can specify a specific version and/or config file - eclipse('4.17').configFile('eclipse-prefs.xml') + cleanthat() + .groupArtifact('io.github.solven-eu.cleanthat:java') // Optional. Default is 'io.github.solven-eu.cleanthat:java' + .version('2.8') // You may force a custom version of Cleanthat + .sourceCompatibility('1.7') // default is '1.7' + .addMutator('SafeAndConsensual') // Default includes the SafeAndConsensual composite mutator + .addMutator('your.custom.MagicMutator') // List of mutators: https://github.com/solven-eu/cleanthat/blob/master/MUTATORS.generated.MD + .excludeMutator('UseCollectionIsEmpty') // You may exclude some mutators (from Composite ones) + .includeDraft(false) // You may exclude draft mutators (from Composite ones) ``` @@ -248,8 +373,8 @@ spotless { ## Groovy -- `com.diffplug.gradle.spotless.GroovyExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/6.2.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/7.0.3/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/7.0.3/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`. @@ -267,13 +392,18 @@ spotless { // optional: instead of specifying import groups directly you can specify a config file // export config file: https://github.com/diffplug/spotless/blob/main/ECLIPSE_SCREENSHOTS.md#creating-spotlessimportorder importOrderFile('eclipse-import-order.txt') // import order file as exported from eclipse - - excludeJava() // excludes all Java sources within the Groovy source dirs from formatting + // removes semicolons at the end of lines + removeSemicolons() // the Groovy Eclipse formatter extends the Java Eclipse formatter, // so it formats Java files by default (unless `excludeJava` is used). greclipse() // has its own section below + + licenseHeader('/* (C) $YEAR */') // or licenseHeaderFile - licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile + //---- Below is for `groovy` only ---- + + // excludes all Java sources within the Groovy source dirs from formatting + excludeJava() } groovyGradle { target '*.gradle' // default target of groovyGradle @@ -284,24 +414,28 @@ spotless { ### eclipse groovy -[homepage](https://github.com/groovy/groovy-eclipse/wiki). [changelog](https://github.com/groovy/groovy-eclipse/releases). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/groovy_eclipse_formatter). The Groovy formatter uses some of the [eclipse jdt](#eclipse-jdt) configuration parameters in addition to groovy-specific ones. All parameters can be configured within a single file, like the Java properties file [greclipse.properties](../testlib/src/main/resources/groovy/greclipse/format/greclipse.properties) in the previous example. The formatter step can also load the [exported Eclipse properties](../ECLIPSE_SCREENSHOTS.md) and augment it with the `.metadata/.plugins/org.eclipse.core.runtime/.settings/org.codehaus.groovy.eclipse.ui.prefs` from your Eclipse workspace as shown below. +[homepage](https://github.com/groovy/groovy-eclipse/wiki). [changelog](https://github.com/groovy/groovy-eclipse/releases). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_groovy_formatter). The Groovy formatter uses some of the [eclipse jdt](#eclipse-jdt) configuration parameters in addition to groovy-specific ones. All parameters can be configured within a single file, like the Java properties file [greclipse.properties](../testlib/src/main/resources/groovy/greclipse/format/greclipse.properties) in the previous example. The formatter step can also load the [exported Eclipse properties](../ECLIPSE_SCREENSHOTS.md) and augment it with the `.metadata/.plugins/org.eclipse.core.runtime/.settings/org.codehaus.groovy.eclipse.ui.prefs` from your Eclipse workspace as shown below. ```gradle spotless { groovy { // Use the default version and Groovy-Eclipse default configuration greclipse() - // optional: you can specify a specific version or config file(s) - // compatible versions: https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/groovy_eclipse_formatter - greclipse('2.3.0').configFile('spotless.eclipseformat.xml', 'org.codehaus.groovy.eclipse.ui.prefs') + // optional: you can specify a specific version or config file(s), version matches the Eclipse Platform + greclipse('4.26').configFile('spotless.eclipseformat.xml', 'org.codehaus.groovy.eclipse.ui.prefs') + // Or supply the configuration as a string + greclipse('4.26').configProperties(""" + ... + """) ``` Groovy-Eclipse formatting errors/warnings lead per default to a build failure. This behavior can be changed by adding the property/key value `ignoreFormatterProblems=true` to a configuration file. In this scenario, files causing problems, will not be modified by this formatter step. ## Kotlin -- `com.diffplug.gradle.spotless.KotlinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/6.2.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/7.0.3/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/7.0.3/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 spotless { // if you are using build.gradle.kts, instead of 'spotless {' use: @@ -315,33 +449,73 @@ spotless { // if you are using build.gradle.kts, instead of 'spotless {' use: licenseHeader '/* (C)$YEAR */' // or licenseHeaderFile } kotlinGradle { - target '*.gradle.kts' // default target for kotlinGradle + target('*.gradle.kts') // default target for kotlinGradle ktlint() // or ktfmt() or prettier() } } ``` +> [!WARNING] +> The target is usually inferred automatically from the java source sets. However, Spotless cannot automatically detect [android](https://github.com/diffplug/spotless/issues/111) or [java-gradle-plugin](https://github.com/diffplug/spotless/issues/437) sources, but you can fix this easily: +> +> ```gradle +> spotless { +> kotlin { +> target 'src/*/kotlin/**/*.kt', 'src/*/java/**/*.kt' +> ``` + ### ktfmt -[homepage](https://github.com/facebookincubator/ktfmt). [changelog](https://github.com/facebookincubator/ktfmt/releases). +[homepage](https://github.com/facebook/ktfmt). [changelog](https://github.com/facebook/ktfmt/releases). ```kotlin spotless { kotlin { - ktfmt('0.30').dropboxStyle() // version and dropbox style are optional + // version, style and all configurations here are optional + ktfmt("0.51").googleStyle().configure { + it.setMaxWidth(80) + it.setBlockIndent(4) + it.setContinuationIndent(4) + it.setRemoveUnusedImports(false) + it.setManageTrailingCommas(false) + } + } +} ``` ### ktlint -[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). Spotless does not ([yet](https://github.com/diffplug/spotless/issues/142)) respect the `.editorconfig` settings ([ktlint docs](https://github.com/pinterest/ktlint#editorconfig)), but you can provide them manually as `userData`. +[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). + +Spotless respects the `.editorconfig` settings by providing `editorConfigPath` option. +([ktlint docs](https://github.com/pinterest/ktlint#editorconfig)). +Default value is the `.editorconfig` file located in the top project. +Passing `null` will clear the option. + +Additionally, `editorConfigOverride` options will override what's supplied in `.editorconfig` file. ```kotlin spotless { kotlin { - // version and userData are both optional - ktlint('0.43.2').userData(mapOf('indent_size' to '2', 'continuation_indent_size' to '2')) + // version, editorConfigPath, editorConfigOverride and customRuleSets are all optional + ktlint("1.0.0") + .setEditorConfigPath("$projectDir/config/.editorconfig") // sample unusual placement + .editorConfigOverride( + mapOf( + "indent_size" to 2, + // intellij_idea is the default style we preset in Spotless, you can override it referring to https://pinterest.github.io/ktlint/latest/rules/code-styles. + "ktlint_code_style" to "intellij_idea", + ) + ) + .customRuleSets( + listOf( + "io.nlopez.compose.rules:ktlint:0.4.16" + ) + ) + } +} ``` ### diktat @@ -359,7 +533,7 @@ spotless { ## Scala -`com.diffplug.gradle.spotless.ScalaExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -383,15 +557,15 @@ spotless { ```gradle spotless { scala { - // version and configFile are both optional - scalafmt('2.6.1').configFile('scalafmt.conf') + // version and configFile, scalaMajorVersion are all optional + scalafmt('3.5.9').configFile('scalafmt.conf').scalaMajorVersion('2.13') ``` ## C/C++ -`com.diffplug.gradle.spotless.CppExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -417,13 +591,17 @@ spotles { cpp { // version and configFile are both optional eclipseCdt('4.13.0').configFile('eclipse-cdt.xml') + // Or supply the configuration as a string + eclipseCdt('4.13.0').configProperties(""" + ... + """) } } ``` ## Python -`com.diffplug.gradle.spotless.PythonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -455,9 +633,43 @@ black().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe') +## Protobuf + +### buf + +`com.diffplug.gradle.spotless.ProtobufExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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. + +```gradle +spotless { + protobuf { + // by default the target is every '.proto' file in the project + buf() + + licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile + } +} +``` + +When used in conjunction with the [buf-gradle-plugin](https://github.com/bufbuild/buf-gradle-plugin), the `buf` executable can be resolved from its `bufTool` configuration: + +```gradle +spotless { + protobuf { + buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath()) + } +} + +// Be sure to disable the buf-gradle-plugin's execution of `buf format`: +buf { + enforceFormat = false +} +``` + ## FreshMark -`com.diffplug.gradle.spotless.FreshMarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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)). @@ -476,9 +688,28 @@ spotless { } ``` +## Flexmark + +`com.diffplug.gradle.spotless.FlexmarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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). + +Currently, none of the available options can be configured yet. It uses only the default options together with `COMMONMARK` as `FORMATTER_EMULATION_PROFILE`. + +To apply flexmark to all of the `.md` files in your project, use this snippet: + +```gradle +spotless { + flexmark { + target '**/*.md' // you have to set the target manually + flexmark() // or flexmark('0.64.8') // version is optional + } +} +``` + ## Antlr4 -`com.diffplug.gradle.spotless.Antlr4Extension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -503,7 +734,7 @@ antlr4formatter('1.2.1') // version is optional ## SQL -`com.diffplug.gradle.spotless.SqlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -523,7 +754,7 @@ spotless { ```gradle spotless { sql { - dbeaver().configFile('dbeaver.props') // configFile is optional + dbeaver().configFile('dbeaver.properties') // configFile is optional ``` Default configuration file, other options [available here](https://github.com/diffplug/spotless/blob/main/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBeaverSQLFormatterConfiguration.java). @@ -539,11 +770,58 @@ sql.formatter.indent.type=space sql.formatter.indent.size=4 ``` +## Maven POM + +`com.diffplug.gradle.spotless.PomExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + pom { + target('pom.xml') // default value, you can change if you want + + sortPom() // has its own section below + } +} +``` + +### sortPom + +[homepage](https://github.com/Ekryd/sortpom). + +All configuration settings are optional, they are described in detail [here](https://github.com/Ekryd/sortpom/wiki/Parameters). + +```gradle +spotless { + pom { + sortPom('4.0.0') + .encoding('UTF-8') // The encoding of the pom files + .lineSeparator(System.getProperty('line.separator')) // line separator to use + .expandEmptyElements(true) // Should empty elements be expanded + .spaceBeforeCloseEmptyElement(false) // Should a space be added inside self-closing elements + .keepBlankLines(true) // Keep empty lines + .endWithNewline(true) // Whether sorted pom ends with a newline + .nrOfIndentSpace(2) // Indentation + .indentBlankLines(false) // Should empty lines be indented + .indentSchemaLocation(false) // Should schema locations be indented + .indentAttribute(null) // Should the xml attributes be indented + .predefinedSortOrder('recommended_2008_06') // Sort order of elements: https://github.com/Ekryd/sortpom/wiki/PredefinedSortOrderProfiles + .sortOrderFile(null) // Custom sort order of elements: https://raw.githubusercontent.com/Ekryd/sortpom/master/sorter/src/main/resources/custom_1.xml + .sortDependencies(null) // Sort dependencies: https://github.com/Ekryd/sortpom/wiki/SortDependencies + .sortDependencyManagement(null) // Sort dependency management: https://github.com/Ekryd/sortpom/wiki/SortDependencies + .sortDependencyExclusions(null) // Sort dependency exclusions: https://github.com/Ekryd/sortpom/wiki/SortDependencies + .sortPlugins(null) // Sort plugins: https://github.com/Ekryd/sortpom/wiki/SortPlugins + .sortProperties(false) // Sort properties + .sortModules(false) // Sort modules + .sortExecutions(false) // Sort plugin executions + } +} +``` + ## Typescript -- `com.diffplug.gradle.spotless.TypescriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { @@ -552,6 +830,8 @@ spotless { tsfmt() // has its own section below prettier() // has its own section below + eslint() // has its own section below + biome() // has its own section below licenseHeader '/* (C) $YEAR */', '(import|const|declare|export|var) ' // or licenseHeaderFile // note the '(import|const|...' argument - this is a regex which identifies the top @@ -582,19 +862,135 @@ spotless { **Prerequisite: tsfmt requires a working NodeJS version** -For details, see the [npm detection](#npm-detection) and [`.npmrc` detection](#npmrc-detection) sections of prettier, which apply also to tsfmt. +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to tsfmt. + + +### ESLint (Typescript) + +[npm](https://www.npmjs.com/package/eslint). [changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md). *Please note:* +The auto-discovery of config files (up the file tree) will not work when using ESLint within spotless, +hence you are required to provide resolvable file paths for config files, or alternatively provide the configuration inline. + +The configuration is very similar to the [ESLint (Javascript)](#eslint-javascript) configuration. In typescript, a +reference to a `tsconfig.json` is required. + +```gradle +spotless { + typescript { + eslint('8.30.0') // version is optional + eslint(['my-eslint-fork': '1.2.3', 'my-eslint-plugin': '1.2.1']) // can specify exactly which npm packages to use + + eslint() + // configuration is mandatory. Provide inline config or a config file. + // a) inline-configuration + .configJs(''' + { + env: { + browser: true, + es2021: true + }, + extends: 'standard-with-typescript', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', + }, + rules: { + } + } + ''') + // b) config file + .configFile('.eslintrc.js') + // recommended: provide a tsconfig.json - especially when using the styleguides + .tsconfigFile('tsconfig.json') + } +} +``` + +**Prerequisite: ESLint requires a working NodeJS version** + +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to ESLint. + +## Javascript + +- `com.diffplug.gradle.spotless.JavascriptExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + javascript { + target 'src/**/*.js' // you have to set the target manually + + prettier() // has its own section below + eslint() // has its own section below + biome() // has its own section below + + licenseHeader '/* (C) $YEAR */', 'REGEX_TO_DEFINE_TOP_OF_FILE' // or licenseHeaderFile + } +} +``` + +### ESLint (Javascript) + +[npm](https://www.npmjs.com/package/eslint). [changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md). *Please note:* +The auto-discovery of config files (up the file tree) will not work when using ESLint within spotless, +hence you are required to provide resolvable file paths for config files, or alternatively provide the configuration inline. + +The configuration is very similar to the [ESLint (Typescript)](#eslint-typescript) configuration. In javascript, *no* +`tsconfig.json` is supported. + +```gradle +spotless { + javascript { + eslint('8.30.0') // version is optional + eslint(['my-eslint-fork': '1.2.3', 'my-eslint-plugin': '1.2.1']) // can specify exactly which npm packages to use + + eslint() + // configuration is mandatory. Provide inline config or a config file. + // a) inline-configuration + .configJs(''' + { + env: { + browser: true, + es2021: true + }, + extends: 'standard', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + } + } + ''') + // b) config file + .configFile('.eslintrc.js') + } +} +``` + +**Prerequisite: ESLint requires a working NodeJS version** + +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to ESLint. ## JSON -- `com.diffplug.gradle.spotless.JsonExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.2.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/7.0.3/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 { json { - target 'src/**/*.json' // you have to set the target manually - simple() // has its own section below + target 'src/**/*.json' // you have to set the target manually + simple() // has its own section below prettier().config(['parser': 'json']) // see Prettier section below - eclipseWtp('json') // see Eclipse web tools platform section + eclipseWtp('json') // see Eclipse web tools platform section + gson() // has its own section below + jackson() // has its own section below + biome() // has its own section below + jsonPatch([]) // has its own section below } } ``` @@ -614,7 +1010,204 @@ spotless { } ``` - +### Gson + +Uses Google Gson to also allow sorting by keys besides custom indentation - useful for i18n files. + +```gradle +spotless { + json { + target 'src/**/*.json' + gson() + .indentWithSpaces(6) // optional: specify the number of spaces to use + .sortByKeys() // optional: sort JSON by its keys + .escapeHtml() // optional: escape HTML in values + .version('2.8.1') // optional: specify version + } +} +``` + +Notes: +* There's no option in Gson to leave HTML as-is (i.e. escaped HTML would remain escaped, raw would remain raw). Either + all HTML characters are written escaped or none. Set `escapeHtml` if you prefer the former. +* `sortByKeys` will apply lexicographic order on the keys of the input JSON. See the + [javadoc of String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#compareTo(java.lang.String)) + for details. + +### Jackson + +Uses Jackson for json files. + +```gradle +spotless { + json { + target 'src/**/*.json' + jackson() + .spaceBeforeSeparator(false) // optional: add a whitespace before key separator. False by default + .feature('INDENT_OUTPUT', true) // optional: true by default + .feature('ORDER_MAP_ENTRIES_BY_KEYS', true) // optional: false by default + .feature('ANY_OTHER_FEATURE', true|false) // optional: any SerializationFeature can be toggled on or off + .jsonFeature('ANY_OTHER_FEATURE', true|false) // any JsonGenerator.Feature can be toggled on or off + } +} +``` + +### jsonPatch + +Uses [zjsonpatch](https://github.com/flipkart-incubator/zjsonpatch) to apply [JSON Patches](https://jsonpatch.com/) as per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) to JSON documents. + +This enables you to add, replace or remove properties at locations in the JSON document that you specify using [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901/). + +In Spotless Gradle, these JSON patches are represented as a `List>`, or a list of patch operations. + +Each patch operation must be a map with the following properties: + +* `"op"` - the operation to apply, one of `"replace"`, `"add"` or `"remove"`. +* `"path"` - a JSON Pointer string, for example `"/foo"` +* `"value"` - the value to `"add"` or `"replace"` at the specified path. Not needed for `"remove"` operations. + +For example, to apply the patch from the [JSON Patch homepage](https://jsonpatch.com/#the-patch): + +```gradle +spotless { + json { + target 'src/**/*.json' + jsonPatch([ + [op: 'replace', path: '/baz', value: 'boo'], + [op: 'add', path: '/hello', value: ['world']], + [op: 'remove', path: '/foo'] + ]) + } +} +``` + +Or using the Kotlin DSL: + +```kotlin +spotless { + json { + target("src/**/*.json") + jsonPatch(listOf( + mapOf("op" to "replace", "path" to "/baz", "value" to "boo"), + mapOf("op" to "add", "path" to "/hello", "value" to listOf("world")), + mapOf("op" to "remove", "path" to "/foo") + )) + } +} +``` + +## YAML + +- `com.diffplug.gradle.spotless.YamlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + yaml { + target 'src/**/*.yaml' // you have to set the target manually + jackson() // has its own section below + prettier() // has its own section below + } +} +``` + +### Jackson + +Uses Jackson for `yaml` files. + +```gradle +spotless { + yaml { + target 'src/**/*.yaml' + jackson() + .spaceBeforeSeparator(false) // optional: add a whitespace before key separator. False by default + .feature('INDENT_OUTPUT', true) // optional: true by default + .feature('ORDER_MAP_ENTRIES_BY_KEYS', true) // optional: false by default + .feature('ANY_OTHER_FEATURE', true|false) // optional: any SerializationFeature can be toggled on or off + .yamlFeature('ANY_OTHER_FEATURE', true|false) // any YAMLGenerator.Feature can be toggled on or off + } +} +``` + +## Shell + +`com.diffplug.gradle.spotless.ShellExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + shell { + target 'scripts/**/*.sh' // default: '**/*.sh' + + shfmt() // has its own section below + } +} +``` + +### shfmt + +[homepage](https://github.com/mvdan/sh). [changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md). + +When formatting shell scripts via `shfmt`, configure `shfmt` settings via `.editorconfig`. +Refer to the `shfmt` [man page](https://github.com/mvdan/sh/blob/master/cmd/shfmt/shfmt.1.scd) for `.editorconfig` settings. + +```gradle +shfmt('3.8.0') // version is optional + +// if shfmt is not on your path, you must specify its location manually +shfmt().pathToExe('/opt/homebrew/bin/shfmt') + +// Spotless always checks the version of the shfmt it is using +// and will fail with an error if it does not match the expected version +// (whether manually specified or default). If there is a problem, Spotless +// will suggest commands to help install the correct version. +// TODO: handle installation & packaging automatically - https://github.com/diffplug/spotless/issues/674 +``` + + + +## Gherkin + +- `com.diffplug.gradle.spotless.GherkinExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + gherkin { + target 'src/**/*.feature' // you have to set the target manually + gherkinUtils() // has its own section below + } +} +``` + +### gherkinUtils + +[homepage](https://github.com/cucumber/gherkin-utils). [changelog](https://github.com/cucumber/gherkin-utils/blob/main/CHANGELOG.md). + +Uses a Gherkin pretty-printer that optionally allows configuring the number of spaces that are used to pretty print objects: + +```gradle +spotless { + gherkin { + target 'src/**/*.feature' // required to be set explicitly + gherkinUtils() + .version('9.0.0') // optional: custom version of 'io.cucumber:gherkin-utils' + } +} +``` + +## CSS + +`com.diffplug.gradle.spotless.CssExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/7.0.3/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 { + css { + target 'css/**/*.css' // default: '**/*.css' + + biome('1.8.3') // has its own section below + } +} +``` + +Note regarding biome: Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). ## Prettier @@ -658,14 +1251,34 @@ To apply prettier to more kinds of files, just add more formats Since spotless uses the actual npm prettier package behind the scenes, it is possible to use prettier with [plugins](https://prettier.io/docs/en/plugins.html#official-plugins) or [community-plugins](https://www.npmjs.com/search?q=prettier-plugin) in order to support even more file types. +#### prettier version below 3 + +```gradle +spotless { + java { + prettier(['prettier': '2.8.8', 'prettier-plugin-java': '2.2.0']).config(['parser': 'java', 'tabWidth': 4]) + } + format 'php', { + target 'src/**/*.php' + prettier(['prettier': '2.8.8', '@prettier/plugin-php': '0.19.6']).config(['parser': 'php', 'tabWidth': 3]) + } +} +``` + +#### prettier version 3+ + +With version 3 prettier it is required to pass in an additional 'plugins' parameter to the config block with a list of plugins you want to use. + ```gradle spotless { java { - prettier(['prettier': '2.0.5', 'prettier-plugin-java': '0.8.0']).config(['parser': 'java', 'tabWidth': 4]) + prettier(['prettier': '3.0.3', 'prettier-plugin-java': '2.3.0']) + .config(['parser': 'java', 'tabWidth': 4, 'plugins': ['prettier-plugin-java']]) } format 'php', { target 'src/**/*.php' - prettier(['prettier': '2.0.5', '@prettier/plugin-php': '0.14.2']).config(['parser': 'php', 'tabWidth': 3]) + prettier(['prettier': '3.0.3', '@prettier/plugin-php': '0.20.1']) + .config(['parser': 'php', 'tabWidth': 3, 'plugins': ['@prettier/plugin-php']]) } } ``` @@ -673,18 +1286,28 @@ spotless { ### npm detection Prettier is based on NodeJS, so a working NodeJS installation (especially npm) is required on the host running spotless. -Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use. +Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm +and/or node binary to use. ```gradle spotless { format 'javascript', { - prettier().npmExecutable('/usr/bin/npm').config(...) + prettier().npmExecutable('/usr/bin/npm').nodeExecutable('/usr/bin/node').config(...) ``` +If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the +two, spotless will assume the other one is in the same directory. + +If you use the `gradle-node-plugin` ([github](https://github.com/node-gradle/gradle-node-plugin)), it is possible to use the +node- and npm-binaries dynamically installed by this plugin. See +[this](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle) +or [this](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle) example. + ### `.npmrc` detection Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home. -Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with `npmExecutable`, of course.) +Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with +`npmExecutable` and `nodeExecutable`, of course.) ```gradle spotless { @@ -692,6 +1315,22 @@ spotless { prettier().npmrc("$projectDir/config/.npmrc").config(...) ``` +### Caching results of `npm install` + +Spotless uses `npm` behind the scenes to install `prettier`. This can be a slow process, especially if you are using a slow internet connection or +if you need large plugins. You can instruct spotless to cache the results of the `npm install` calls, so that for the next installation, +it will not need to download the packages again, but instead reuse the cached version. + +```gradle +spotless { + typescript { + prettier().npmInstallCache() // will use the default cache directory (the build-directory of the respective module) + prettier().npmInstallCache("${rootProject.rootDir}/.gradle/spotless-npm-cache") // will use the specified directory (creating it if not existing) +``` + +Depending on your filesystem and the location of the cache directory, spotless will use hardlinks when caching the npm packages. If that is not +possible, it will fall back to copying the files. + ## clang-format [homepage](https://clang.llvm.org/docs/ClangFormat.html). [changelog](https://releases.llvm.org/download.html). `clang-format` is a formatter for c, c++, c#, objective-c, protobuf, javascript, and java. You can use clang-format in any language-specific format, but usually you will be creating a generic format. @@ -728,9 +1367,9 @@ spotless { ```gradle spotless { format 'xml', { - target 'src/**/*/xml' // must specify target + target 'src/**/*.xml' // must specify target eclipseWtp('xml') // must specify a type (table below) - eclipseWtp('xml', '4.13.0') // optional version + eclipseWtp('xml', '11.0') // optional version, others at https://download.eclipse.org/tools/cdt/releases/ // you can also specify an arbitrary number of config files eclipseWtp('xml').configFile('spotless.xml.prefs', 'spotless.common.properties' } @@ -760,6 +1399,177 @@ Unlike Eclipse, Spotless WTP ignores per default external URIs in schema locatio external entities. To allow the access of external URIs, set the property `resolveExternalURI` to true. +## Biome + +[homepage](https://biomejs.dev/). [changelog](https://github.com/biomejs/biome/blob/main/CHANGELOG.md). Biome is +a formatter that for the frontend written in Rust, which has a native binary, does not require Node.js and as such, +is pretty fast. It can currently format JavaScript, TypeScript, JSX, and JSON, and may support +[more frontend languages](https://biomejs.dev/internals/language-support/) such as CSS in the future. + +You can use Biome in any language-specific format for supported languages, but +usually you will be creating a generic format. + +Note regarding biome: Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). + +```gradle +spotless { + format 'styling', { + // you have to set the target manually + target 'src/*/webapp/**/*.js' + + // Download Biome from the network if not already downloaded, see below for more info + biome('1.2.0') + + // (optional) Path to the directory with the biome.json conig file + biome('1.2.0').configPath("path/config/dir") + + // (optional) Biome will auto detect the language based on the file extension. + // See below for possible values. + biome('1.2.0').language("js") + } +} +``` + +To apply Biome to more kinds of files with a different configuration, just add +more formats: + +```gradle +spotless { + format 'biome-js', { + target '**/*.js' + biome('1.2.0') + } + format 'biome-ts', { + target '**/*.ts' + biome('1.2.0') + } + format 'biome-json', { + target '**/*.json' + biome('1.2.0') + } +} +``` + +**Limitations:** + +- The auto-discovery of config files (up the file tree) will not work when using + Biome within spotless. +- The `ignore` option of the `biome.json` configuration file will not be applied. + Include and exclude patterns are configured in the spotless configuration in the + Gradle settings file instead. + +Note: Due to a limitation of biome, if the name of a file matches a pattern in +the `ignore` option of the specified `biome.json` configuration file, it will not be +formatted, even if included in the biome configuration section of the Gradle settings +file. +You could specify a different `biome.json` configuration file without an `ignore` +pattern to circumvent this. + +Note 2: Biome is hard-coded to ignore certain special files, such as `package.json` +or `tsconfig.json`. These files will never be formatted. + +### Biome binary + +To format with Biome, spotless needs to find the Biome binary. By default, +spotless downloads the binary for the given version from the network. This +should be fine in most cases, but may not work e.g. when there is not connection +to the internet. + +To download the Biome binary from the network, just specify a version: + +```gradle +spotless { + format 'biome', { + target '**/*.js','**/*.ts','**/*.json' + biome('1.2.0') + } +} +``` + +Spotless uses a default version when you do not specify a version, but this +may change at any time, so we recommend that you always set the Biome version +you want to use. Optionally, you can also specify a directory for the downloaded +Biome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/biome`): + +```gradle +spotless { + format 'biome', { + target '**/*.js','**/*.ts','**/*.json' + // Relative paths are resolved against the project's base directory + biome('1.2.0').downloadDir("${project.gradle.gradleUserHomeDir}/biome") + } +} +``` + +To use a fixed binary, omit the `version` and specify a `pathToExe`: + +```gradle +spotless { + format 'biome', { + target '**/*.js','**/*.ts','**/*.json' + biome().pathToExe("${project.layout.buildDirectory.asFile.get().absolutePath}/bin/biome") + } +} +``` + +Absolute paths are used as-is. Relative paths are resolved against the project's +base directory. To use a pre-installed Biome binary on the user's path, specify +just a name without any slashes / backslashes: + +```gradle +spotless { + format 'biome', { + target '**/*.js','**/*.ts','**/*.json' + // Uses the "biome" command, which must be on the user's path. --> + biome().pathToExe('biome') + } +} +``` + +### Biome configuration file + +Biome is a biased formatter and linter without many options, but there are a few +basic options. Biome uses a file named [biome.json](https://biomejs.dev/reference/configuration/) +for its configuration. When none is specified, the default configuration from +Biome is used. To use a custom configuration: + +```gradle +spotless { + format 'biome', { + target '**/*.js','**/*.ts','**/*.json' + // Must point to the directory with the "biome.json" config file --> + // Relative paths are resolved against the project's base directory --> + biome('1.2.0').configPath('./config') + } +} +``` + +### Biome input language + +By default, Biome detects the language / syntax of the files to format +automatically from the file extension. This may fail if your source code files +have unusual extensions for some reason. If you are using the generic format, +you can force a certain language like this: + +```xml +spotless { + format 'biome', { + target 'src/**/typescript/**/*.mjson' + biome('1.2.0').language('json') + } +} +``` + +The following languages are currently recognized: + +* `js` -- JavaScript +* `jsx` -- JavaScript + JSX (React) +* `js?` -- JavaScript, with or without JSX, depending on the file extension +* `ts` -- TypeScript +* `tsx` -- TypeScript + JSX (React) +* `ts?` -- TypeScript, with or without JSX, depending on the file extension +* `json` -- JSON + ## Generic steps [Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these: @@ -786,7 +1596,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/6.2.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/7.0.3/com/diffplug/gradle/spotless/FormatExtension.LicenseHeaderConfig.html) for a complete listing of options. @@ -794,6 +1604,12 @@ See the [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-g If your project has not been rigorous with copyright headers, and you'd like to use git history to repair this retroactively, you can do so with `-PspotlessSetLicenseHeaderYearsFromGitHistory=true`. When run in this mode, Spotless will do an expensive search through git history for each file, and set the copyright header based on the oldest and youngest commits for that file. This is intended to be a one-off sort of thing. +### Files with fixed header lines + +Some files have fixed header lines (e.g. ` ## How can I enforce formatting gradually? (aka "ratchet") @@ -811,6 +1627,13 @@ However, we strongly recommend that you use a non-local branch, such as a tag or This is especially helpful for injecting accurate copyright dates using the [license step](#license-header). +### Using `ratchetFrom` on CI systems + +Many popular CI systems (GitHub, GitLab, BitBucket, and Travis) use a "shallow clone". This means that `ratchetFrom 'origin/main'` will fail with `No such reference`. You can fix this by: + +- calling `git fetch origin main` before you call Spotless +- disabling the shallow clone [like so](https://github.com/diffplug/spotless/issues/710) + ## `spotless:off` and `spotless:on` Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this: @@ -836,12 +1659,18 @@ spotless { encoding 'Cp1252' // except java, which will be Cp1252 ``` -Line endings can also be set globally or per-format using the `lineEndings` property. Spotless supports four line ending modes: `UNIX`, `WINDOWS`, `PLATFORM_NATIVE`, and `GIT_ATTRIBUTES`. The default value is `GIT_ATTRIBUTES`, and *we highly recommend that you* ***do not change*** *this value*. Git has opinions about line endings, and if Spotless and git disagree, then you're going to have a bad time. +Line endings can also be set globally or per-format using the `lineEndings` property. Spotless supports + +- constant modes (`UNIX`, `WINDOWS`, `MAC_CLASSIC`) +- simple modes (`PLATFORM_NATIVE`, `PRESERVE`) +- and git-aware modes (`GIT_ATTRIBUTES`, `GIT_ATTRIBUTES_FAST_ALLSAME`) + +The default value is `GIT_ATTRIBUTES_FAST_ALLSAME`, and *we highly recommend that you* ***do not change*** *this value*. Git has opinions about line endings, and if Spotless and git disagree, then you're going to have a bad time. `FAST_ALLSAME` just means that Spotless can assume that every file being formatted has the same line endings ([more info](https://github.com/diffplug/spotless/pull/1838)). You can easily set the line endings of different files using [a `.gitattributes` file](https://help.github.com/articles/dealing-with-line-endings/). Here's an example `.gitattributes` which sets all files to unix newlines: `* text eol=lf`. - + ## Custom steps @@ -853,9 +1682,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/6.2.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/7.0.3/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/6.2.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/7.0.3/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 @@ -888,11 +1717,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/6.2.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/7.0.3/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/6.2.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/7.0.3/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 @@ -985,6 +1814,7 @@ If you use this feature, you will get an error if you use a formatter in a subpr * [Goomph](https://github.com/diffplug/goomph) ([direct link to spotless section in its build.gradle](https://github.com/diffplug/goomph/blob/v1.0.0/build.gradle#L78-L99)) * [FreshMark](https://github.com/diffplug/freshmark) ([direct link to spotless section in its build.gradle](https://github.com/diffplug/freshmark/blob/v1.3.0/build.gradle#L52-L73)) * [JScriptBox](https://github.com/diffplug/jscriptbox) ([direct link to spotless section in its build.gradle](https://github.com/diffplug/jscriptbox/blob/v3.0.0/build.gradle#L45-L65)) +* [Compose Guard](https://github.com/j-roskopf/ComposeGuard) ([direct link to spotless section in its build.gradle.kts](https://github.com/j-roskopf/ComposeGuard/blob/main/compose-guard/build.gradle.kts#L28-L48)) * (Your project here) diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index 3a5afd2ba8..5211428f8b 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -1,17 +1,17 @@ apply from: rootProject.file('gradle/changelog.gradle') ext.artifactId = project.artifactIdGradle version = spotlessChangelog.versionNext -apply from: rootProject.file('gradle/java-setup.gradle') -apply from: rootProject.file('gradle/spotless-freshmark.gradle') - 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 project(':lib') - api project(':lib-extra') + 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}" @@ -21,76 +21,67 @@ dependencies { implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}" - testImplementation project(':testlib') + testImplementation projects.testlib testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" + testImplementation 'org.owasp.encoder:encoder:1.3.1' + testRuntimeOnly "org.junit.platform:junit-platform-launcher" } -test { - useJUnitPlatform() +apply from: rootProject.file('gradle/special-tests.gradle') +tasks.withType(Test).configureEach { testLogging.showStandardStreams = true } - -apply from: rootProject.file('gradle/special-tests.gradle') +tasks.validatePlugins { + 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' + ]) } spotlessPluginLegacy { id = 'com.diffplug.gradle.spotless' implementationClass = 'com.diffplug.gradle.spotless.SpotlessPluginRedirect' displayName = 'Spotless formatting plugin (legacy)' description = project.description + tags.set([ + 'format' + ]) } } } -if (version.endsWith('-SNAPSHOT')) { - publishPlugins.enabled = false -} else { - pluginBundle { - // These settings are set for the whole plugin bundle - website = "https://github.com/diffplug/spotless" - vcsUrl = "https://github.com/diffplug/spotless" - tags = [ - 'format', - 'style', - 'license', - 'header', - 'google-java-format', - 'eclipse', - 'ktlint', - 'ktfmt', - 'diktat', - 'tsfmt', - 'prettier', - 'scalafmt', - 'scalafix', - 'black', - 'clang-format' - ] - plugins { - spotlessPlugin { - id = 'com.diffplug.spotless' - } - spotlessPluginLegacy { - id = 'com.diffplug.gradle.spotless' - } - } - mavenCoordinates { - groupId = project.group - artifactId = project.artifactIdGradle - version = project.version - } - } + +tasks.named("publishPlugins") { + enabled = !version.endsWith('-SNAPSHOT') + notCompatibleWithConfigurationCache("https://github.com/gradle/gradle/issues/21283") } // have to apply java-publish after setting up the pluginBundle diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java new file mode 100644 index 0000000000..2185f3693a --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 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.util.Collections; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.json.JacksonConfig; +import com.diffplug.spotless.json.JacksonJsonStep; + +public abstract class AJacksonGradleConfig { + protected final FormatExtension formatExtension; + + protected JacksonConfig jacksonConfig; + + protected String version = JacksonJsonStep.defaultVersion(); + + // Make sure to call 'formatExtension.addStep(createStep());' in the extented constructors + public AJacksonGradleConfig(JacksonConfig jacksonConfig, FormatExtension formatExtension) { + this.formatExtension = formatExtension; + + this.jacksonConfig = jacksonConfig; + } + + public T feature(String feature, boolean toggle) { + this.jacksonConfig.appendFeatureToToggle(Collections.singletonMap(feature, toggle)); + formatExtension.replaceStep(createStep()); + return self(); + } + + public T version(String version) { + this.version = version; + formatExtension.replaceStep(createStep()); + return self(); + } + + public abstract T self(); + + protected abstract FormatterStep createStep(); +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java new file mode 100644 index 0000000000..83d678c46c --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseGroovyExtension.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023-2024 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 com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.gradle.api.Project; + +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.groovy.GrEclipseFormatterStep; +import com.diffplug.spotless.groovy.RemoveSemicolonsStep; +import com.diffplug.spotless.java.ImportOrderStep; + +public abstract class BaseGroovyExtension extends FormatExtension { + protected BaseGroovyExtension(SpotlessExtension spotless) { + super(spotless); + } + + public void importOrder(String... importOrder) { + addStep(ImportOrderStep.forGroovy().createFrom(importOrder)); + } + + public void importOrderFile(Object importOrderFile) { + Objects.requireNonNull(importOrderFile); + addStep(ImportOrderStep.forGroovy().createFrom(getProject().file(importOrderFile))); + } + + public void removeSemicolons() { + addStep(RemoveSemicolonsStep.create()); + } + + public GrEclipseConfig greclipse() { + return greclipse(GrEclipseFormatterStep.defaultVersion()); + } + + public GrEclipseConfig greclipse(String version) { + return new GrEclipseConfig(version, this); + } + + public static class GrEclipseConfig { + private final EquoBasedStepBuilder builder; + private final FormatExtension extension; + + private GrEclipseConfig(String version, FormatExtension extension) { + this.extension = extension; + builder = GrEclipseFormatterStep.createBuilder(extension.provisioner()); + builder.setVersion(version); + extension.addStep(builder.build()); + } + + public GrEclipseConfig configFile(Object... configFiles) { + requireElementsNonNull(configFiles); + Project project = extension.getProject(); + builder.setPreferences(project.files(configFiles).getFiles()); + extension.replaceStep(builder.build()); + return this; + } + + public GrEclipseConfig configProperties(String... configs) { + requireElementsNonNull(configs); + builder.setPropertyPreferences(List.of(configs)); + extension.replaceStep(builder.build()); + return this; + } + + public GrEclipseConfig withP2Mirrors(Map mirrors) { + builder.setP2Mirrors(mirrors); + extension.replaceStep(builder.build()); + return this; + } + } +} 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 new file mode 100644 index 0000000000..8d9a36c804 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -0,0 +1,209 @@ +/* + * Copyright 2023-2024 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.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import com.diffplug.common.collect.ImmutableList; +import com.diffplug.common.collect.ImmutableSortedMap; +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.kotlin.DiktatStep; +import com.diffplug.spotless.kotlin.KtLintStep; +import com.diffplug.spotless.kotlin.KtfmtStep; + +public abstract class BaseKotlinExtension extends FormatExtension { + protected BaseKotlinExtension(SpotlessExtension spotless) { + super(spotless); + } + + public DiktatConfig diktat() { + return diktat(DiktatStep.defaultVersionDiktat()); + } + + /** Adds the specified version of diktat. */ + public DiktatConfig diktat(String version) { + return new DiktatConfig(version); + } + + public KtlintConfig ktlint() throws IOException { + return ktlint(KtLintStep.defaultVersion()); + } + + /** Adds the specified version of ktlint. */ + public KtlintConfig ktlint(String version) throws IOException { + return new KtlintConfig(version, Collections.emptyMap(), Collections.emptyList()); + } + + /** 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 + * option to format source code. + */ + public KtfmtConfig ktfmt(String version) { + return new KtfmtConfig(version); + } + + protected abstract boolean isScript(); + + public class DiktatConfig { + private final String version; + private FileSignature config; + + private DiktatConfig(String version) { + Objects.requireNonNull(version, "version"); + this.version = version; + addStep(createStep()); + } + + public DiktatConfig configFile(Object file) throws IOException { + // Specify the path to the configuration file + if (file == null) { + this.config = null; + } else { + this.config = FileSignature.signAsList(getProject().file(file)); + } + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return DiktatStep.create(version, provisioner(), isScript(), config); + } + } + + public class KtfmtConfig { + private final String version; + private final ConfigurableStyle configurableStyle = new ConfigurableStyle(); + private KtfmtStep.Style style; + private KtfmtStep.KtfmtFormattingOptions options; + + private KtfmtConfig(String version) { + Objects.requireNonNull(version); + this.version = Objects.requireNonNull(version); + addStep(createStep()); + } + + public ConfigurableStyle metaStyle() { + return style(KtfmtStep.Style.META); + } + + public ConfigurableStyle dropboxStyle() { + return style(KtfmtStep.Style.DROPBOX); + } + + public ConfigurableStyle googleStyle() { + return style(KtfmtStep.Style.GOOGLE); + } + + public ConfigurableStyle kotlinlangStyle() { + return style(KtfmtStep.Style.KOTLINLANG); + } + + public void configure(Consumer optionsConfiguration) { + this.configurableStyle.configure(optionsConfiguration); + } + + private ConfigurableStyle style(KtfmtStep.Style style) { + this.style = style; + replaceStep(createStep()); + return configurableStyle; + } + + private FormatterStep createStep() { + return KtfmtStep.create(version, provisioner(), style, options); + } + + public class ConfigurableStyle { + public void configure(Consumer optionsConfiguration) { + KtfmtStep.KtfmtFormattingOptions ktfmtFormattingOptions = new KtfmtStep.KtfmtFormattingOptions(); + optionsConfiguration.accept(ktfmtFormattingOptions); + options = ktfmtFormattingOptions; + replaceStep(createStep()); + } + } + } + + public class KtlintConfig { + private final String version; + private FileSignature editorConfigPath; + private Map editorConfigOverride; + private List customRuleSets; + + private KtlintConfig( + String version, + Map editorConfigOverride, + List customRuleSets) throws IOException { + Objects.requireNonNull(version); + File defaultEditorConfig = getProject().getRootProject().file(".editorconfig"); + FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; + this.version = version; + this.editorConfigPath = editorConfigPath; + this.editorConfigOverride = editorConfigOverride; + this.customRuleSets = customRuleSets; + addStep(createStep()); + } + + public KtlintConfig setEditorConfigPath(@Nullable Object editorConfigPath) throws IOException { + if (editorConfigPath == null) { + this.editorConfigPath = null; + } else { + File editorConfigFile = getProject().file(editorConfigPath); + if (!editorConfigFile.exists()) { + throw new IllegalArgumentException("EditorConfig file does not exist: " + editorConfigFile); + } + this.editorConfigPath = FileSignature.signAsList(editorConfigFile); + } + replaceStep(createStep()); + return this; + } + + public KtlintConfig editorConfigOverride(Map editorConfigOverride) { + // Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized + // representation. + this.editorConfigOverride = ImmutableSortedMap.copyOf(editorConfigOverride); + replaceStep(createStep()); + return this; + } + + public KtlintConfig customRuleSets(List customRuleSets) { + this.customRuleSets = ImmutableList.copyOf(customRuleSets); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return KtLintStep.create( + version, + provisioner(), + editorConfigPath, + editorConfigOverride, + customRuleSets); + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BiomeStepConfig.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BiomeStepConfig.java new file mode 100644 index 0000000000..776c844d9b --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BiomeStepConfig.java @@ -0,0 +1,266 @@ +/* + * Copyright 2023-2024 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 java.util.Objects.requireNonNull; + +import java.io.File; +import java.nio.file.Paths; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import org.gradle.api.Project; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.biome.BiomeStep; + +public abstract class BiomeStepConfig> { + /** + * Optional path to the directory with configuration file for Biome. The file + * must be named {@code biome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + */ + @Nullable + private Object configPath; + + /** + * Optional directory where the downloaded Biome executable is placed. If this + * is a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/biome. + */ + @Nullable + private Object downloadDir; + + /** + * The flavor of Biome to use. Will be removed when we stop support the + * deprecated Rome project. + */ + private final BiomeFlavor flavor; + + /** + * Optional path to the Biome executable. Either a version or a + * pathToExe should be specified. When not given, an attempt is + * made to download the executable for the given version from the network. When + * given, the executable is used and the version parameter is + * ignored. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * biome) is given, this is interpreted as the name of a command + * with executable that is in your path environment variable. Use + * ./executable-name if you want to use an executable in the + * project's base directory. + */ + @Nullable + private Object pathToExe; + + /** + * A reference to the Gradle project for which spotless is executed. + */ + private final Project project; + + /** + * Replaces the current Biome formatter step with the given step. + */ + private final Consumer replaceStep; + + /** + * Biome version to download, applies only when no pathToExe is + * specified explicitly. Either a version or a + * pathToExe should be specified. When not given, a default known + * version is used. For stable builds, it is recommended that you always set the + * version explicitly. This parameter is ignored when you specify a + * pathToExe explicitly. + */ + @Nullable + private String version; + + protected BiomeStepConfig(Project project, Consumer replaceStep, BiomeFlavor flavor, + String version) { + this.project = requireNonNull(project); + this.replaceStep = requireNonNull(replaceStep); + this.flavor = flavor; + this.version = version; + } + + /** + * Optional path to the directory with configuration file for Biome. The file + * must be named {@code biome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + * + * @return This step for further configuration. + */ + public Self configPath(Object configPath) { + this.configPath = configPath; + replaceStep(); + return getThis(); + } + + /** + * Optional directory where the downloaded Biome executable is placed. If this + * is a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/biome. + * + * @return This step for further configuration. + */ + public Self downloadDir(Object downloadDir) { + this.downloadDir = downloadDir; + replaceStep(); + return getThis(); + } + + /** + * Optional path to the Biome executable. Overwrites the configured version. No + * attempt is made to download the Biome executable from the network. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * biome) is given, this is interpreted as the name of a command + * with executable that is in your path environment variable. Use + * ./executable-name if you want to use an executable in the + * project's base directory. + * + * @return This step for further configuration. + */ + public Self pathToExe(Object pathToExe) { + this.pathToExe = pathToExe; + replaceStep(); + return getThis(); + } + + /** + * Creates a new formatter step that formats code by calling the Biome + * executable, using the current configuration. + * + * @return A new formatter step for the Biome formatter. + */ + protected FormatterStep createStep() { + var builder = newBuilder(); + if (configPath != null) { + var resolvedConfigPath = project.file(configPath); + builder.withConfigPath(resolvedConfigPath.toString()); + } + builder.withLanguage(getLanguage()); + return builder.create(); + } + + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently, the following languages are supported by Biome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ * + * @return The language of the input files. + */ + protected abstract String getLanguage(); + + /** + * @return This Biome config instance. + */ + protected abstract Self getThis(); + + /** + * Creates a new Biome step and replaces the existing Biome step in the list of + * format steps. + */ + protected void replaceStep() { + replaceStep.accept(createStep()); + } + + /** + * Finds the data directory that can be used for storing shared data such as + * Biome executable globally. This is a directory in the local repository, e.g. + * ~/.m2/repository/com/diffplus/spotless/spotless-data. + * + * @return The directory for storing shared data. + */ + private File findDataDir() { + // e.g. ~/.gradle/ + var userHomeDir = project.getGradle().getGradleUserHomeDir().toPath(); + var dataPath = userHomeDir.resolve("com").resolve("diffplug").resolve("spotless").resolve("spotless-data"); + return dataPath.toAbsolutePath().toFile(); + } + + /** + * A new builder for configuring a Biome step that either downloads the Biome + * executable with the given version from the network, or uses the executable + * from the given path. + * + * @return A builder for a Biome step. + */ + private BiomeStep newBuilder() { + if (pathToExe != null) { + var resolvedPathToExe = resolvePathToExe(); + return BiomeStep.withExePath(flavor, resolvedPathToExe); + } else { + var downloadDir = resolveDownloadDir(); + return BiomeStep.withExeDownload(flavor, version, downloadDir); + } + } + + /** + * Resolves the path to the Biome executable. When the path is only a file name, + * do not perform any resolution and interpret it as a command that must be on + * the user's path. Otherwise, resolve the executable path against the project's + * base directory. + * + * @return The resolved path to the Biome executable. + */ + private String resolvePathToExe() { + var fileNameOnly = pathToExe instanceof String && Paths.get(pathToExe.toString()).getNameCount() == 1; + if (fileNameOnly) { + return pathToExe.toString(); + } else { + return project.file(pathToExe).toString(); + } + } + + /** + * Resolves the directory to use for storing downloaded Biome executable. When a + * {@link #downloadDir} is given, use that directory, resolved against the + * current project's directory. Otherwise, use the {@code biome} sub folder in + * the shared data directory. + * + * @return The download directory for the Biome executable. + */ + private String resolveDownloadDir() { + if (downloadDir != null) { + return project.file(downloadDir).toString(); + } else { + return findDataDir().toPath().resolve(flavor.shortName()).toString(); + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java index 64dabc9ca5..f2d6ee91bb 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CppExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,16 @@ import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; +import java.util.List; +import java.util.Map; + import javax.inject.Inject; import org.gradle.api.Project; +import com.diffplug.gradle.spotless.JavaExtension.EclipseConfig; import com.diffplug.spotless.cpp.CppDefaults; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; import com.diffplug.spotless.extra.cpp.EclipseCdtFormatterStep; public class CppExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { @@ -42,7 +46,7 @@ public EclipseConfig eclipseCdt(String version) { } public class EclipseConfig { - private final EclipseBasedStepBuilder builder; + private final EquoBasedStepBuilder builder; EclipseConfig(String version) { builder = EclipseCdtFormatterStep.createBuilder(provisioner()); @@ -50,11 +54,25 @@ public class EclipseConfig { addStep(builder.build()); } - public void configFile(Object... configFiles) { + public EclipseConfig configFile(Object... configFiles) { requireElementsNonNull(configFiles); Project project = getProject(); builder.setPreferences(project.files(configFiles).getFiles()); replaceStep(builder.build()); + return this; + } + + public EclipseConfig configProperties(String... configs) { + requireElementsNonNull(configs); + builder.setPropertyPreferences(List.of(configs)); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig withP2Mirrors(Map mirrors) { + builder.setP2Mirrors(mirrors); + replaceStep(builder.build()); + return this; } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CssExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CssExtension.java new file mode 100644 index 0000000000..f3dd15ccb3 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/CssExtension.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 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 javax.inject.Inject; + +import com.diffplug.spotless.biome.BiomeFlavor; + +/** Gradle step for formatting CSS files. */ +public class CssExtension extends FormatExtension { + private static final String CSS_FILE_EXTENSION = "**/*.css"; + + static final String NAME = "css"; + + @Inject + public CssExtension(SpotlessExtension spotless) { + super(spotless); + } + + /** If the user hasn't specified files, assume all CSS files should be checked. */ + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target = parseTarget(CSS_FILE_EXTENSION); + } + super.setupTask(task); + } + + /** + * Adds the default version of the biome formatter. + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + */ + public BiomeCss biome() { + return biome(null); + } + + /** + * Adds the given version of the biome formatter. + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + * @param version Biome version to use. + */ + public BiomeCss biome(String version) { + var biomeConfig = new BiomeCss(version); + addStep(biomeConfig.createStep()); + return biomeConfig; + } + + /** + * Biome formatter step for CSS. + */ + public class BiomeCss extends BiomeStepConfig { + /** + * Creates a new Biome formatter step config for formatting CSS files. Unless + * overwritten, the given Biome version is downloaded from the network. + * + * @param version Biome version to use. + */ + public BiomeCss(String version) { + super(getProject(), CssExtension.this::replaceStep, BiomeFlavor.BIOME, version); + } + + @Override + protected String getLanguage() { + return "css"; + } + + @Override + protected BiomeCss getThis() { + return this; + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java new file mode 100644 index 0000000000..d1f941613b --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FlexmarkExtension.java @@ -0,0 +1,64 @@ +/* + * Copyright 2023 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.util.Objects; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.markdown.FlexmarkStep; + +public class FlexmarkExtension extends FormatExtension { + static final String NAME = "flexmark"; + + @Inject + public FlexmarkExtension(SpotlessExtension spotless) { + super(spotless); + } + + public FlexmarkFormatterConfig flexmark() { + return flexmark(FlexmarkStep.defaultVersion()); + } + + public FlexmarkFormatterConfig flexmark(String version) { + return new FlexmarkFormatterConfig(version); + } + + @Override + protected void setupTask(SpotlessTask task) { + // defaults to all markdown files + if (target == null) { + throw noDefaultTargetException(); + } + super.setupTask(task); + } + + public class FlexmarkFormatterConfig { + + private final String version; + + FlexmarkFormatterConfig(String version) { + this.version = Objects.requireNonNull(version); + addStep(createStep()); + } + + private FormatterStep createStep() { + return FlexmarkStep.create(this.version, provisioner()); + } + } + +} 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 9da66b7929..69150724c1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,24 @@ package com.diffplug.gradle.spotless; import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; +import static com.diffplug.gradle.spotless.SpotlessPluginRedirect.badSemver; +import static com.diffplug.gradle.spotless.SpotlessPluginRedirect.badSemverOfGradle; +import static java.util.Objects.requireNonNull; import java.io.File; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Random; import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.inject.Inject; @@ -36,24 +42,33 @@ import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileTree; +import org.gradle.api.file.Directory; import org.gradle.api.file.FileCollection; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.util.GradleVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.diffplug.common.base.Preconditions; -import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintSuppression; +import com.diffplug.spotless.OnMatch; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.SerializedFunction; +import com.diffplug.spotless.biome.BiomeFlavor; import com.diffplug.spotless.cpp.ClangFormatStep; import com.diffplug.spotless.extra.EclipseBasedStepBuilder; import com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep; import com.diffplug.spotless.generic.EndWithNewlineStep; +import com.diffplug.spotless.generic.FenceStep; import com.diffplug.spotless.generic.IndentStep; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode; import com.diffplug.spotless.generic.NativeCmdStep; -import com.diffplug.spotless.generic.PipeStepPair; import com.diffplug.spotless.generic.ReplaceRegexStep; import com.diffplug.spotless.generic.ReplaceStep; import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; @@ -64,12 +79,15 @@ /** Adds a {@code spotless{Name}Check} and {@code spotless{Name}Apply} task. */ public class FormatExtension { + + private static final Logger logger = LoggerFactory.getLogger(FormatExtension.class); + final SpotlessExtension spotless; final List> lazyActions = new ArrayList<>(); @Inject public FormatExtension(SpotlessExtension spotless) { - this.spotless = Objects.requireNonNull(spotless); + this.spotless = requireNonNull(spotless); } protected final Provisioner provisioner() { @@ -87,29 +105,44 @@ private String formatName() { LineEnding lineEndings; - /** Returns the line endings to use (defaults to {@link SpotlessExtensionImpl#getLineEndings()}. */ + /** + * Returns the line endings to use (defaults to + * {@link SpotlessExtensionImpl#getLineEndings()}. + */ public LineEnding getLineEndings() { return lineEndings == null ? spotless.getLineEndings() : lineEndings; } - /** Sets the line endings to use (defaults to {@link SpotlessExtensionImpl#getLineEndings()}. */ + /** + * Sets the line endings to use (defaults to + * {@link SpotlessExtensionImpl#getLineEndings()}. + */ public void setLineEndings(LineEnding lineEndings) { - this.lineEndings = Objects.requireNonNull(lineEndings); + this.lineEndings = requireNonNull(lineEndings); } Charset encoding; - /** Returns the encoding to use (defaults to {@link SpotlessExtensionImpl#getEncoding()}. */ + /** + * Returns the encoding to use (defaults to + * {@link SpotlessExtensionImpl#getEncoding()}. + */ public Charset getEncoding() { return encoding == null ? spotless.getEncoding() : encoding; } - /** Sets the encoding to use (defaults to {@link SpotlessExtensionImpl#getEncoding()}. */ + /** + * Sets the encoding to use (defaults to + * {@link SpotlessExtensionImpl#getEncoding()}. + */ public void setEncoding(String name) { - setEncoding(Charset.forName(Objects.requireNonNull(name))); + setEncoding(Charset.forName(requireNonNull(name))); } - /** Sentinel to distinguish between "don't ratchet this format" and "use spotless parent format". */ + /** + * Sentinel to distinguish between "don't ratchet this format" and "use spotless + * parent format". + */ private static final String RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL = " not set at format level "; private String ratchetFrom = RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL; @@ -120,8 +153,8 @@ public String getRatchetFrom() { } /** - * Allows you to override the value from the parent {@link SpotlessExtension#setRatchetFrom(String)} - * for this specific format. + * Allows you to override the value from the parent + * {@link SpotlessExtension#setRatchetFrom(String)} for this specific format. */ public void setRatchetFrom(String ratchetFrom) { this.ratchetFrom = ratchetFrom; @@ -132,24 +165,50 @@ public void ratchetFrom(String ratchetFrom) { setRatchetFrom(ratchetFrom); } - /** Sets the encoding to use (defaults to {@link SpotlessExtensionImpl#getEncoding()}. */ + /** + * Sets the encoding to use (defaults to + * {@link SpotlessExtensionImpl#getEncoding()}. + */ public void setEncoding(Charset charset) { - encoding = Objects.requireNonNull(charset); + encoding = requireNonNull(charset); } - final FormatExceptionPolicyStrict exceptionPolicy = new FormatExceptionPolicyStrict(); + final List lintSuppressions = new ArrayList<>(); + + /** Suppresses any lints which meet the supplied criteria. */ + public void suppressLintsFor(Action lintSuppression) { + LintSuppression suppression = new LintSuppression(); + lintSuppression.execute(suppression); + suppression.ensureDoesNotSuppressAll(); + lintSuppressions.add(suppression); + } - /** Ignores errors in the given step. */ + /** + * Ignores errors in the given step. + * + * @deprecated Use {@link #suppressLintsFor(Action)} instead. + */ + @Deprecated public void ignoreErrorForStep(String stepName) { - exceptionPolicy.excludeStep(Objects.requireNonNull(stepName)); + System.err.println("`ignoreErrorForStep('" + stepName + "') is deprecated, use `suppressLintsFor { step = '" + stepName + "' }` instead."); + suppressLintsFor(it -> it.setStep(stepName)); } - /** Ignores errors for the given relative path. */ + /** + * Ignores errors for the given relative path. + * + * @deprecated Use {@link #suppressLintsFor(Action)} instead. + */ + @Deprecated public void ignoreErrorForPath(String relativePath) { - exceptionPolicy.excludePath(Objects.requireNonNull(relativePath)); + System.err.println("`ignoreErrorForPath('" + relativePath + "') is deprecated, use `suppressLintsFor { path = '" + relativePath + "' }` instead."); + suppressLintsFor(it -> it.setPath(relativePath)); } - /** Sets encoding to use (defaults to {@link SpotlessExtensionImpl#getEncoding()}). */ + /** + * Sets encoding to use (defaults to + * {@link SpotlessExtensionImpl#getEncoding()}). + */ public void encoding(String charset) { setEncoding(charset); } @@ -157,6 +216,10 @@ public void encoding(String charset) { /** The files to be formatted = (target - targetExclude). */ protected FileCollection target, targetExclude; + /** The value from which files will be excluded if their content contain it. */ + @Nullable + protected String targetExcludeContentPattern = null; + protected boolean isLicenseHeaderStep(FormatterStep formatterStep) { String formatterStepName = formatterStep.getName(); @@ -168,36 +231,56 @@ protected boolean isLicenseHeaderStep(FormatterStep formatterStep) { } /** - * Sets which files should be formatted. Files to be formatted = (target - targetExclude). - * + * Sets which files should be formatted. Files to be formatted = (target - + * targetExclude). + *

* When this method is called multiple times, only the last call has any effect. - * - * FileCollections pass through raw. - * Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir. - * List are treated as the 'includes' arg to fileTree, with project.rootDir as the dir. - * Anything else gets passed to getProject().files(). - * - * If you pass any strings that start with "**\/*", this method will automatically filter out - * "build", ".gradle", and ".git" folders. + *

+ * FileCollections pass through raw. Strings are treated as the 'include' arg to + * fileTree, with project.rootDir as the dir. List are treated as the + * 'includes' arg to fileTree, with project.rootDir as the dir. Anything else + * gets passed to getProject().files(). + *

+ * If you pass any strings that start with "**\/*", this method will + * automatically filter out "build", ".gradle", and ".git" folders. */ public void target(Object... targets) { this.target = parseTargetsIsExclude(targets, false); } /** - * Sets which files will be excluded from formatting. Files to be formatted = (target - targetExclude). - * + * Sets which files will be excluded from formatting. Files to be formatted = + * (target - targetExclude). + *

* When this method is called multiple times, only the last call has any effect. - * - * FileCollections pass through raw. - * Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir. - * List are treated as the 'includes' arg to fileTree, with project.rootDir as the dir. - * Anything else gets passed to getProject().files(). + *

+ * FileCollections pass through raw. Strings are treated as the 'include' arg to + * fileTree, with project.rootDir as the dir. List are treated as the + * 'includes' arg to fileTree, with project.rootDir as the dir. Anything else + * gets passed to getProject().files(). */ public void targetExclude(Object... targets) { this.targetExclude = parseTargetsIsExclude(targets, true); } + /** + * Excludes all files whose content contains {@code string}. + *

+ * When this method is called multiple times, only the last call has any effect. + */ + public void targetExcludeIfContentContains(String string) { + targetExcludeIfContentContainsRegex(Pattern.quote(string)); + } + + /** + * Excludes all files whose content contains the given regex. + *

+ * When this method is called multiple times, only the last call has any effect. + */ + public void targetExcludeIfContentContainsRegex(String regex) { + this.targetExcludeContentPattern = regex; + } + private FileCollection parseTargetsIsExclude(Object[] targets, boolean isExclude) { requireElementsNonNull(targets); if (targets.length == 0) { @@ -214,10 +297,10 @@ private FileCollection parseTargetsIsExclude(Object[] targets, boolean isExclude } /** - * FileCollections pass through raw. - * Strings are treated as the 'include' arg to fileTree, with project.rootDir as the dir. - * List are treated as the 'includes' arg to fileTree, with project.rootDir as the dir. - * Anything else gets passed to getProject().files(). + * FileCollections pass through raw. Strings are treated as the 'include' arg to + * fileTree, with project.rootDir as the dir. List are treated as the + * 'includes' arg to fileTree, with project.rootDir as the dir. Anything else + * gets passed to getProject().files(). */ protected final FileCollection parseTarget(Object target) { return parseTargetIsExclude(target, false); @@ -225,7 +308,7 @@ protected final FileCollection parseTarget(Object target) { private final FileCollection parseTargetIsExclude(Object target, boolean isExclude) { if (target instanceof Collection) { - return parseTargetsIsExclude(((Collection) target).toArray(), isExclude); + return parseTargetsIsExclude(((Collection) target).toArray(), isExclude); } else if (target instanceof FileCollection) { return (FileCollection) target; } else if (target instanceof String) { @@ -234,7 +317,8 @@ private final FileCollection parseTargetIsExclude(Object target, boolean isExclu String targetString = (String) target; matchedFiles.include(targetString); - // since people are likely to do '**/*.md', we want to make sure to exclude folders + // since people are likely to do '**/*.md', we want to make sure to exclude + // folders // they don't want to format which will slow down the operation greatly // but we only want to do that if they are *including* - if they are specifying // what they want to exclude, we shouldn't filter at all @@ -249,10 +333,11 @@ private final FileCollection parseTargetIsExclude(Object target, boolean isExclu if (getProject() == getProject().getRootProject()) { excludes.add(".gradle"); } - // no build folders (flatInclude means that subproject might not be subfolders, see https://github.com/diffplug/spotless/issues/121) - relativizeIfSubdir(excludes, dir, getProject().getBuildDir()); + // no build folders (flatInclude means that subproject might not be subfolders, + // see https://github.com/diffplug/spotless/issues/121) + relativizeIfSubdir(excludes, dir, getProject().getLayout().getBuildDirectory().getAsFile().get()); for (Project subproject : getProject().getSubprojects()) { - relativizeIfSubdir(excludes, dir, subproject.getBuildDir()); + relativizeIfSubdir(excludes, dir, subproject.getLayout().getBuildDirectory().getAsFile().get()); } matchedFiles.exclude(excludes); } @@ -270,8 +355,8 @@ private static void relativizeIfSubdir(List relativePaths, File root, Fi } /** - * Returns the relative path between root and dest, - * or null if dest is not a child of root. + * Returns the relative path between root and dest, or null if dest is not a + * child of root. */ static @Nullable String relativize(File root, File dest) { String rootPath = root.getAbsolutePath(); @@ -279,7 +364,8 @@ private static void relativizeIfSubdir(List relativePaths, File root, Fi if (!destPath.startsWith(rootPath)) { return null; } else { - return destPath.substring(rootPath.length()); + String relativized = destPath.substring(rootPath.length()); + return relativized.startsWith("/") || relativized.startsWith("\\") ? relativized.substring(1) : relativized; } } @@ -288,15 +374,26 @@ private static void relativizeIfSubdir(List relativePaths, File root, Fi /** Adds a new step. */ public void addStep(FormatterStep newStep) { - Objects.requireNonNull(newStep); + requireNonNull(newStep); int existingIdx = getExistingStepIdx(newStep.getName()); if (existingIdx != -1) { - throw new GradleException("Multiple steps with name '" + newStep.getName() + "' for spotless format '" + formatName() + "'"); + throw new GradleException( + "Multiple steps with name '" + newStep.getName() + "' for spotless format '" + formatName() + "'"); } steps.add(newStep); } - /** Returns the index of the existing step with the given name, or -1 if no such step exists. */ + /** Adds a new step that requires a Provisioner. */ + public void addStep(Function createStepFn) { + requireNonNull(createStepFn); + FormatterStep newStep = createStepFn.apply(provisioner()); + addStep(newStep); + } + + /** + * Returns the index of the existing step with the given name, or -1 if no such + * step exists. + */ protected int getExistingStepIdx(String stepName) { for (int i = 0; i < steps.size(); ++i) { if (steps.get(i).getName().equals(stepName)) { @@ -310,7 +407,8 @@ protected int getExistingStepIdx(String stepName) { protected void replaceStep(FormatterStep replacementStep) { int existingIdx = getExistingStepIdx(replacementStep.getName()); if (existingIdx == -1) { - throw new GradleException("Cannot replace step '" + replacementStep.getName() + "' for spotless format '" + formatName() + "' because it hasn't been added yet."); + throw new GradleException("Cannot replace step '" + replacementStep.getName() + "' for spotless format '" + + formatName() + "' because it hasn't been added yet."); } steps.set(existingIdx, replacementStep); } @@ -321,20 +419,21 @@ public void clearSteps() { } /** - * An optional performance optimization if you are using any of the {@code custom} - * methods. If you aren't explicitly calling {@code custom}, then this method - * has no effect. - * - * Spotless tracks what files have changed from run to run, so that it can run faster - * by only checking files which have changed, or whose formatting steps have changed. - * If you use the {@code custom} methods, then gradle can never mark - * your files as {@code up-to-date}, because it can't know if perhaps the behavior of your - * custom function has changed. - * - * If you set {@code bumpThisNumberIfACustomStepChanges( )}, then spotless will - * assume that the custom rules have not changed if the number has not changed. If a - * custom rule does change, then you must bump the number so that spotless will know - * that it must recheck the files it has already checked. + * An optional performance optimization if you are using any of the + * {@code custom} methods. If you aren't explicitly calling {@code custom}, then + * this method has no effect. + *

+ * Spotless tracks what files have changed from run to run, so that it can run + * faster by only checking files which have changed, or whose formatting steps + * have changed. If you use the {@code custom} methods, then Gradle can never + * mark your files as {@code up-to-date}, because it can't know if perhaps the + * behavior of your custom function has changed. + *

+ * If you set {@code bumpThisNumberIfACustomStepChanges( )}, then + * spotless will assume that the custom rules have not changed if the number has + * not changed. If a custom rule does change, then you must bump the number so + * that spotless will know that it must recheck the files it has already + * checked. */ public void bumpThisNumberIfACustomStepChanges(int number) { globalState = number; @@ -352,16 +451,49 @@ protected Integer calculateState() throws Exception { } } - /** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */ + /** + * Adds a custom step. Receives a string with unix-newlines, must return a + * string with unix newlines. + */ public void custom(String name, Closure formatter) { - Objects.requireNonNull(formatter, "formatter"); - custom(name, formatter::call); + requireNonNull(formatter, "formatter"); + custom(name, new ClosureFormatterFunc(formatter)); + } + + static class ClosureFormatterFunc implements FormatterFunc, Serializable { + private Closure closure; + + ClosureFormatterFunc(Closure closure) { + this.closure = closure; + } + + @Override + public String apply(String unixNewlines) { + return closure.call(unixNewlines); + } + + private void writeObject(java.io.ObjectOutputStream stream) throws java.io.IOException { + stream.writeObject(closure.dehydrate()); + } + + private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { + this.closure = (Closure) stream.readObject(); + } } - /** Adds a custom step. Receives a string with unix-newlines, must return a string with unix newlines. */ + /** + * Adds a custom step. Receives a string with unix-newlines, must return a + * string with unix newlines. + */ public void custom(String name, FormatterFunc formatter) { - Objects.requireNonNull(formatter, "formatter"); - addStep(FormatterStep.createLazy(name, () -> globalState, unusedState -> formatter)); + requireNonNull(formatter, "formatter"); + if (badSemverOfGradle() < badSemver(SpotlessPlugin.VER_GRADLE_minVersionForCustom)) { + throw new GradleException("The 'custom' method is only available if you are using Gradle " + + SpotlessPlugin.VER_GRADLE_minVersionForCustom + + " or newer, this is " + + GradleVersion.current().getVersion()); + } + addStep(FormatterStep.createLazy(name, () -> globalState, SerializedFunction.alwaysReturns(formatter))); } /** Highly efficient find-replace char sequence. */ @@ -385,34 +517,64 @@ public void endWithNewline() { } /** Ensures that the files are indented using spaces. */ + public void leadingTabsToSpaces(int spacesPerTab) { + addStep(IndentStep.Type.SPACE.create(spacesPerTab)); + } + + @Deprecated public void indentWithSpaces(int numSpacesPerTab) { - addStep(IndentStep.Type.SPACE.create(numSpacesPerTab)); + logDeprecation("indentWithSpaces", "leadingTabsToSpaces"); + leadingTabsToSpaces(numSpacesPerTab); } /** Ensures that the files are indented using spaces. */ - public void indentWithSpaces() { + public void leadingTabsToSpaces() { addStep(IndentStep.Type.SPACE.create()); } + @Deprecated + public void indentWithSpaces() { + logDeprecation("indentWithSpaces", "leadingTabsToSpaces"); + leadingTabsToSpaces(); + } + /** Ensures that the files are indented using tabs. */ + public void leadingSpacesToTabs(int spacesPerTab) { + addStep(IndentStep.Type.TAB.create(spacesPerTab)); + } + + @Deprecated public void indentWithTabs(int tabToSpaces) { - addStep(IndentStep.Type.TAB.create(tabToSpaces)); + logDeprecation("indentWithTabs", "leadingSpacesToTabs"); + leadingSpacesToTabs(tabToSpaces); } /** Ensures that the files are indented using tabs. */ - public void indentWithTabs() { + public void leadingSpacesToTabs() { addStep(IndentStep.Type.TAB.create()); } + @Deprecated + public void indentWithTabs() { + logDeprecation("indentWithTabs", "leadingSpacesToTabs"); + leadingSpacesToTabs(); + } + + private static void logDeprecation(String methodName, String replacement) { + logger.warn("'{}' is deprecated, use '{}' in your gradle build script instead.", methodName, replacement); + } + /** Ensures formatting of files via native binary. */ public void nativeCmd(String name, String pathToExe, List arguments) { addStep(NativeCmdStep.create(name, new File(pathToExe), arguments)); } /** - * Created by {@link FormatExtension#licenseHeader(String, String)} or {@link FormatExtension#licenseHeaderFile(Object, String)}. - * For most language-specific formats (e.g. java, scala, etc.) you can omit the second {@code delimiter} argument, because it is supplied - * automatically ({@link HasBuiltinDelimiterForLicense}). + * Created by {@link FormatExtension#licenseHeader(String, String)} or + * {@link FormatExtension#licenseHeaderFile(Object, String)}. For most + * language-specific formats (e.g. java, scala, etc.) you can omit the second + * {@code delimiter} argument, because it is supplied automatically + * ({@link HasBuiltinDelimiterForLicense}). */ public class LicenseHeaderConfig { LicenseHeaderStep builder; @@ -441,8 +603,8 @@ public LicenseHeaderConfig(LicenseHeaderStep builder) { } /** - * @param delimiter - * Spotless will look for a line that starts with this regular expression pattern to know what the "top" is. + * @param delimiter Spotless will look for a line that starts with this regular + * expression pattern to know what the "top" is. */ public LicenseHeaderConfig delimiter(String delimiter) { builder = builder.withDelimiter(delimiter); @@ -451,8 +613,8 @@ public LicenseHeaderConfig delimiter(String delimiter) { } /** - * @param yearSeparator - * The characters used to separate the first and last years in multi years patterns. + * @param yearSeparator The characters used to separate the first and last years + * in multi years patterns. */ public LicenseHeaderConfig yearSeparator(String yearSeparator) { builder = builder.withYearSeparator(yearSeparator); @@ -460,10 +622,18 @@ public LicenseHeaderConfig yearSeparator(String yearSeparator) { return this; } + public LicenseHeaderConfig skipLinesMatching(String skipLinesMatching) { + builder = builder.withSkipLinesMatching(skipLinesMatching); + replaceStep(createStep()); + return this; + } + /** - * @param updateYearWithLatest - * Will turn {@code 2004} into {@code 2004-2020}, and {@code 2004-2019} into {@code 2004-2020} - * Default value is false, unless {@link SpotlessExtensionImpl#ratchetFrom(String)} is used, in which case default value is true. + * @param updateYearWithLatest Will turn {@code 2004} into {@code 2004-2020}, + * and {@code 2004-2019} into {@code 2004-2020} + * Default value is false, unless + * {@link SpotlessExtensionImpl#ratchetFrom(String)} + * is used, in which case default value is true. */ public LicenseHeaderConfig updateYearWithLatest(boolean updateYearWithLatest) { this.updateYearWithLatest = updateYearWithLatest; @@ -473,7 +643,8 @@ public LicenseHeaderConfig updateYearWithLatest(boolean updateYearWithLatest) { FormatterStep createStep() { return builder.withYearModeLazy(() -> { - if ("true".equals(spotless.project.findProperty(LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()))) { + if ("true".equals(spotless.project + .findProperty(LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()))) { return YearMode.SET_FROM_GIT; } else { boolean updateYear = updateYearWithLatest == null ? getRatchetFrom() != null : updateYearWithLatest; @@ -484,22 +655,22 @@ FormatterStep createStep() { } /** - * @param licenseHeader - * Content that should be at the top of every file. - * @param delimiter - * Spotless will look for a line that starts with this regular expression pattern to know what the "top" is. + * @param licenseHeader Content that should be at the top of every file. + * @param delimiter Spotless will look for a line that starts with this + * regular expression pattern to know what the "top" is. */ public LicenseHeaderConfig licenseHeader(String licenseHeader, String delimiter) { - LicenseHeaderConfig config = new LicenseHeaderConfig(LicenseHeaderStep.headerDelimiter(licenseHeader, delimiter)); + LicenseHeaderConfig config = new LicenseHeaderConfig( + LicenseHeaderStep.headerDelimiter(licenseHeader, delimiter)); addStep(config.createStep()); return config; } /** - * @param licenseHeaderFile - * Content that should be at the top of every file. - * @param delimiter - * Spotless will look for a line that starts with this regular expression pattern to know what the "top" is. + * @param licenseHeaderFile Content that should be at the top of every file. + * @param delimiter Spotless will look for a line that starts with this + * regular expression pattern to know what the "top" + * is. */ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile, String delimiter) { LicenseHeaderConfig config = new LicenseHeaderConfig(LicenseHeaderStep.headerDelimiter(() -> { @@ -511,23 +682,61 @@ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile, String de return config; } - public abstract class NpmStepConfig> { + public abstract static class NpmStepConfig> { + + public static final String SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME = "spotless-npm-install-cache"; + @Nullable protected Object npmFile; + @Nullable + protected Object nodeFile; + + @Nullable + protected Object npmInstallCache; + @Nullable protected Object npmrcFile; + protected Project project; + + private Consumer replaceStep; + + public NpmStepConfig(Project project, Consumer replaceStep) { + this.project = requireNonNull(project); + this.replaceStep = requireNonNull(replaceStep); + } + @SuppressWarnings("unchecked") public T npmExecutable(final Object npmFile) { this.npmFile = npmFile; - replaceStep(createStep()); + replaceStep(); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T nodeExecutable(final Object nodeFile) { + this.nodeFile = nodeFile; + replaceStep(); return (T) this; } public T npmrc(final Object npmrcFile) { this.npmrcFile = npmrcFile; - replaceStep(createStep()); + replaceStep(); + return (T) this; + } + + public T npmInstallCache(final Object npmInstallCache) { + this.npmInstallCache = npmInstallCache; + replaceStep(); + return (T) this; + } + + public T npmInstallCache() { + this.npmInstallCache = new File(project.getLayout().getBuildDirectory().getAsFile().get(), + SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME); + replaceStep(); return (T) this; } @@ -535,15 +744,27 @@ File npmFileOrNull() { return fileOrNull(npmFile); } + File nodeFileOrNull() { + return fileOrNull(nodeFile); + } + File npmrcFileOrNull() { return fileOrNull(npmrcFile); } + File npmModulesCacheOrNull() { + return fileOrNull(npmInstallCache); + } + private File fileOrNull(Object npmFile) { - return npmFile != null ? getProject().file(npmFile) : null; + return npmFile != null ? project.file(npmFile) : null; + } + + protected void replaceStep() { + replaceStep.accept(createStep()); } - abstract FormatterStep createStep(); + abstract protected FormatterStep createStep(); } @@ -558,34 +779,93 @@ public class PrettierConfig extends NpmStepConfig { final Map devDependencies; PrettierConfig(Map devDependencies) { - this.devDependencies = Objects.requireNonNull(devDependencies); + super(getProject(), FormatExtension.this::replaceStep); + this.devDependencies = requireNonNull(devDependencies); } public PrettierConfig configFile(final Object prettierConfigFile) { this.prettierConfigFile = prettierConfigFile; - replaceStep(createStep()); + replaceStep(); return this; } public PrettierConfig config(final Map prettierConfig) { this.prettierConfig = new TreeMap<>(prettierConfig); - replaceStep(createStep()); + replaceStep(); return this; } - FormatterStep createStep() { + @Override + protected FormatterStep createStep() { final Project project = getProject(); - return PrettierFormatterStep.create( - devDependencies, - provisioner(), - project.getBuildDir(), - new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()), + return PrettierFormatterStep.create(devDependencies, provisioner(), project.getProjectDir(), + project.getLayout().getBuildDirectory().getAsFile().get(), npmModulesCacheOrNull(), + new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), + Arrays.asList(project.getProjectDir(), project.getRootDir())), new com.diffplug.spotless.npm.PrettierConfig( this.prettierConfigFile != null ? project.file(this.prettierConfigFile) : null, this.prettierConfig)); } } + /** + * Generic Biome formatter step that detects the language of the input file from + * the file name. It should be specified as a formatter step for a generic + * format{ ... }. + */ + public class BiomeGeneric extends BiomeStepConfig { + @Nullable + String language; + + /** + * Creates a new Biome config that downloads the Biome executable for the given + * version from the network. + * + * @param version Biome version to use. The default version is used when + * null. + */ + public BiomeGeneric(String version) { + super(getProject(), FormatExtension.this::replaceStep, BiomeFlavor.BIOME, version); + } + + /** + * Sets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Biome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ * + * @param language The language of the files to format. + * @return This step for further configuration. + */ + public BiomeGeneric language(String language) { + this.language = language; + replaceStep(); + return this; + } + + @Override + protected String getLanguage() { + return language; + } + + @Override + protected BiomeGeneric getThis() { + return this; + } + } + /** Uses the default version of prettier. */ public PrettierConfig prettier() { return prettier(PrettierFormatterStep.defaultDevDependencies()); @@ -603,6 +883,22 @@ public PrettierConfig prettier(Map devDependencies) { return prettierConfig; } + /** + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + */ + public BiomeStepConfig biome() { + return biome(null); + } + + /** Downloads the given Biome version from the network. */ + public BiomeStepConfig biome(String version) { + var biomeConfig = new BiomeGeneric(version); + addStep(biomeConfig.createStep()); + return biomeConfig; + } + /** Uses the default version of clang-format. */ public ClangFormatConfig clangFormat() { return clangFormat(ClangFormatStep.defaultVersion()); @@ -680,8 +976,9 @@ public void withinBlocks(String name, String open, String close, Action * spotless { @@ -689,76 +986,94 @@ public void withinBlocks(String name, String open, String close, Action */ - public void withinBlocks(String name, String open, String close, Class clazz, Action configure) { - withinBlocksHelper(PipeStepPair.named(name).openClose(open, close), clazz, configure); + public void withinBlocks(String name, String open, String close, Class clazz, + Action configure) { + withinBlocksHelper(FenceStep.named(name).openClose(open, close), clazz, configure); } - /** Same as {@link #withinBlocks(String, String, String, Action)}, except instead of an open/close pair, you specify a regex with exactly one capturing group. */ + /** + * Same as {@link #withinBlocks(String, String, String, Action)}, except instead + * of an open/close pair, you specify a regex with exactly one capturing group. + */ public void withinBlocksRegex(String name, String regex, Action configure) { withinBlocksRegex(name, regex, FormatExtension.class, configure); } - /** Same as {@link #withinBlocksRegex(String, String, Action)}, except you can specify any language-specific subclass of {@link FormatExtension} to get language-specific steps. */ - public void withinBlocksRegex(String name, String regex, Class clazz, Action configure) { - withinBlocksHelper(PipeStepPair.named(name).regex(regex), clazz, configure); + /** + * Same as {@link #withinBlocksRegex(String, String, Action)}, except you can + * specify any language-specific subclass of {@link FormatExtension} to get + * language-specific steps. + */ + public void withinBlocksRegex(String name, String regex, Class clazz, + Action configure) { + withinBlocksHelper(FenceStep.named(name).regex(regex), clazz, configure); } - private void withinBlocksHelper(PipeStepPair.Builder builder, Class clazz, Action configure) { + private void withinBlocksHelper(FenceStep fence, Class clazz, + Action configure) { // create the sub-extension T formatExtension = spotless.instantiateFormatExtension(clazz); // configure it configure.execute(formatExtension); // create a step which applies all of those steps as sub-steps - FormatterStep step = builder.buildStepWhichAppliesSubSteps(spotless.project.getRootDir().toPath(), formatExtension.steps); + FormatterStep step = fence.applyWithin(formatExtension.steps); addStep(step); } /** - * Given a regex with *exactly one capturing group*, disables formatting - * inside that captured group. + * Given a regex with *exactly one capturing group*, disables formatting inside + * that captured group. */ public void toggleOffOnRegex(String regex) { - this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).regex(regex).buildPair(); + this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).regex(regex); } /** Disables formatting between the given tags. */ public void toggleOffOn(String off, String on) { - this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair(); + this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).openClose(off, on); } /** Disables formatting between {@code spotless:off} and {@code spotless:on}. */ public void toggleOffOn() { - toggleOffOn(PipeStepPair.defaultToggleOff(), PipeStepPair.defaultToggleOn()); + toggleOffOn(FenceStep.defaultToggleOff(), FenceStep.defaultToggleOn()); } - /** Undoes all previous calls to {@link #toggleOffOn()} and {@link #toggleOffOn(String, String)}. */ + /** + * Undoes all previous calls to {@link #toggleOffOn()} and + * {@link #toggleOffOn(String, String)}. + */ public void toggleOffOnDisable() { - this.togglePair = null; + this.toggleFence = null; } - private @Nullable PipeStepPair togglePair; + private @Nullable FenceStep toggleFence; /** Sets up a format task according to the values in this extension. */ protected void setupTask(SpotlessTask task) { task.setEncoding(getEncoding().name()); - task.setExceptionPolicy(exceptionPolicy); + task.setLintSuppressions(lintSuppressions); FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude); task.setTarget(totalTarget); List steps; - if (togglePair != null) { - steps = new ArrayList<>(this.steps.size() + 2); - steps.add(togglePair.in()); - steps.addAll(this.steps); - steps.add(togglePair.out()); + if (toggleFence != null) { + steps = List.of(toggleFence.preserveWithin(this.steps)); } else { steps = this.steps; } + if (targetExcludeContentPattern != null) { + steps.replaceAll( + formatterStep -> formatterStep.filterByContent(OnMatch.EXCLUDE, targetExcludeContentPattern)); + } task.setSteps(steps); - task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget)); + Directory projectDir = getProject().getLayout().getProjectDirectory(); + LineEnding lineEndings = getLineEndings(); + task.setLineEndingsPolicy( + getProject().provider(() -> lineEndings.createPolicy(projectDir.getAsFile(), () -> totalTarget))); spotless.getRegisterDependenciesTask().hookSubprojectTask(task); task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : ""); } @@ -768,32 +1083,44 @@ protected Project getProject() { return spotless.project; } + /** Eager version of {@link #createIndependentApplyTaskLazy(String)} */ + public SpotlessApply createIndependentApplyTask(String taskName) { + return createIndependentApplyTaskLazy(taskName).get(); + } + /** - * Creates an independent {@link SpotlessApply} for (very) unusual circumstances. - * - * Most users will not want this method. In the rare case that you want to create - * a {@code SpotlessApply} which is independent of the normal Spotless machinery, this will - * let you do that. - * - * The returned task will not be hooked up to the global {@code spotlessApply}, and there will be no corresponding {@code check} task. - * + * Creates an independent {@link SpotlessApply} for (very) unusual + * circumstances. + *

+ * Most users will not want this method. In the rare case that you want to + * create a {@code SpotlessApply} which is independent of the normal Spotless + * machinery, this will let you do that. + *

+ * The returned task will not be hooked up to the global {@code spotlessApply}, + * and there will be no corresponding {@code check} task. + *

* The task name must not end with `Apply`. - * - * NOTE: does not respect the rarely-used {@code spotlessFiles} property. + *

+ * NOTE: does not respect the rarely-used {@code spotlessFiles} + * property. */ - public SpotlessApply createIndependentApplyTask(String taskName) { - Preconditions.checkArgument(!taskName.endsWith(SpotlessExtension.APPLY), "Task name must not end with " + SpotlessExtension.APPLY); - // create and setup the task - SpotlessTaskImpl spotlessTask = spotless.project.getTasks().create(taskName + SpotlessTaskService.INDEPENDENT_HELPER, SpotlessTaskImpl.class); - spotlessTask.init(spotless.getRegisterDependenciesTask().getTaskService()); - setupTask(spotlessTask); - // clean removes the SpotlessCache, so we have to run after clean - SpotlessPlugin.configureCleanTask(spotless.project, spotlessTask::mustRunAfter); + public TaskProvider createIndependentApplyTaskLazy(String taskName) { + Preconditions.checkArgument(!taskName.endsWith(SpotlessExtension.APPLY), + "Task name must not end with " + SpotlessExtension.APPLY); + TaskProvider spotlessTask = spotless.project.getTasks() + .register(taskName + SpotlessTaskService.INDEPENDENT_HELPER, SpotlessTaskImpl.class, task -> { + task.init(spotless.getRegisterDependenciesTask().getTaskService()); + setupTask(task); + // clean removes the SpotlessCache, so we have to run after clean + task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME); + }); // create the apply task - SpotlessApply applyTask = spotless.project.getTasks().create(taskName, SpotlessApply.class); - applyTask.init(spotlessTask); - applyTask.dependsOn(spotlessTask); - + TaskProvider applyTask = spotless.project.getTasks().register(taskName, SpotlessApply.class, + task -> { + task.dependsOn(spotlessTask); + task.init(spotlessTask.get()); + }); return applyTask; } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java index b046beda7c..71904b2bb4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.TreeMap; import javax.inject.Inject; @@ -38,13 +38,7 @@ public class FreshMarkExtension extends FormatExtension { @Inject public FreshMarkExtension(SpotlessExtension spotless) { super(spotless); - addStep(FreshMarkStep.create(() -> { - Map map = new HashMap<>(); - for (Action> action : propertyActions) { - action.execute(map); - } - return map; - }, provisioner())); + addStep(FreshMarkStep.create(Map.of(), provisioner())); } public void properties(Action> action) { @@ -67,6 +61,10 @@ protected void setupTask(SpotlessTask task) { if (target == null) { throw noDefaultTargetException(); } + // replace the step + TreeMap props = new TreeMap<>(); + propertyActions.forEach(action -> action.execute(props)); + replaceStep(FreshMarkStep.create(props, provisioner())); super.setupTask(task); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java new file mode 100644 index 0000000000..6305289818 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GherkinExtension.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2023 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 javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.gherkin.GherkinUtilsStep; + +public class GherkinExtension extends FormatExtension { + static final String NAME = "gherkin"; + + @Inject + public GherkinExtension(SpotlessExtension spotless) { + super(spotless); + } + + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + throw noDefaultTargetException(); + } + super.setupTask(task); + } + + public GherkinUtilsConfig gherkinUtils() { + return new GherkinUtilsConfig(); + } + + public class GherkinUtilsConfig { + private String version; + private int indent; + + public GherkinUtilsConfig() { + this.version = GherkinUtilsStep.defaultVersion(); + this.indent = com.diffplug.spotless.gherkin.GherkinUtilsConfig.defaultIndentSpaces(); + addStep(createStep()); + } + + public void version(String version) { + this.version = version; + replaceStep(createStep()); + } + + private FormatterStep createStep() { + return GherkinUtilsStep.create(new com.diffplug.spotless.gherkin.GherkinUtilsConfig(indent), version, provisioner()); + } + } + +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java index 8c4f88e7da..b2c3adc459 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchetGradle.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,33 @@ import javax.annotation.Nullable; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.SystemReader; + import com.diffplug.spotless.extra.GitRatchet; /** Gradle implementation of GitRatchet. */ public class GitRatchetGradle extends GitRatchet { + static { + preventJGitFromCallingExecutables(); + } + + static void preventJGitFromCallingExecutables() { + SystemReader reader = SystemReader.getInstance(); + SystemReader.setInstance(new DelegatingSystemReader(reader) { + @Override + public String getenv(String variable) { + if ("PATH".equals(variable)) { + return ""; + } else { + return super.getenv(variable); + } + } + }); + } + @Override protected File getDir(File project) { return project; @@ -32,4 +55,52 @@ protected File getDir(File project) { protected @Nullable File getParent(File project) { return project.getParentFile(); } + + static class DelegatingSystemReader extends SystemReader { + final SystemReader reader; + + DelegatingSystemReader(SystemReader reader) { + this.reader = reader; + } + + @Override + public String getHostname() { + return reader.getHostname(); + } + + @Override + public String getenv(String variable) { + return reader.getProperty(variable); + } + + @Override + public String getProperty(String key) { + return reader.getProperty(key); + } + + @Override + public FileBasedConfig openUserConfig(Config parent, FS fs) { + return reader.openUserConfig(parent, fs); + } + + @Override + public FileBasedConfig openSystemConfig(Config parent, FS fs) { + return reader.openSystemConfig(parent, fs); + } + + @Override + public FileBasedConfig openJGitConfig(Config parent, FS fs) { + return reader.openJGitConfig(parent, fs); + } + + @Override + public long getCurrentTime() { + return reader.getCurrentTime(); + } + + @Override + public int getTimezone(long when) { + return reader.getTimezone(when); + } + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GoExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GoExtension.java new file mode 100644 index 0000000000..145e90c19a --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GoExtension.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.go.GofmtFormatStep; + +public class GoExtension extends FormatExtension { + public static final String NAME = "go"; + + @Inject + public GoExtension(SpotlessExtension spotless) { + super(spotless); + } + + public GofmtConfig gofmt() { + return new GofmtConfig(GofmtFormatStep.defaultVersion()); + } + + public GofmtConfig gofmt(String version) { + return new GofmtConfig(version); + } + + public class GofmtConfig { + GofmtFormatStep stepCfg; + + public GofmtConfig(String version) { + stepCfg = GofmtFormatStep.withVersion(version); + addStep(createStep()); + } + + public GofmtConfig withGoExecutable(String pathToGo) { + stepCfg = stepCfg.withGoExecutable(pathToGo); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return stepCfg.create(); + } + } +} 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 240ffcdcdf..5d5c07f4fe 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,18 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.Attribute; import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; import org.gradle.api.initialization.dsl.ScriptHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.diffplug.common.base.Unhandled; import com.diffplug.common.collect.ImmutableList; @@ -118,8 +120,14 @@ private static Provisioner forConfigurationContainer(Project project, Configurat .forEach(config.getDependencies()::add); config.setDescription(mavenCoords.toString()); config.setTransitive(withTransitives); + config.setCanBeConsumed(false); + 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)); + // TODO: This is a copy-paste from org.gradle.api.attributes.java.TargetJvmEnvironment which is added in Gradle 7.0, remove this once we drop support for Gradle 6.x. + // Add this attribute for resolving Guava dependency, see https://github.com/google/guava/issues/6801. + attr.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); }); return config.resolve(); } catch (Exception e) { @@ -127,17 +135,15 @@ private static Provisioner forConfigurationContainer(Project project, Configurat if (!projName.isEmpty()) { projName = projName + "/"; } - logger.log( - Level.SEVERE, - "You need to add a repository containing the '" + mavenCoords + "' artifact in '" + projName + "build.gradle'.\n" + + throw new GradleException(String.format( + "You need to add a repository containing the '%s' artifact in '%sbuild.gradle'.%n" + "E.g.: 'repositories { mavenCentral() }'", - e); - throw e; + mavenCoords, projName), e); } }; } - private static final Logger logger = Logger.getLogger(GradleProvisioner.class.getName()); + private static final Logger logger = LoggerFactory.getLogger(GradleProvisioner.class); /** Models a request to the provisioner. */ private static class Request { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java index 5e528e1b79..676bed0aea 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,19 @@ */ package com.diffplug.gradle.spotless; -import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; - -import java.util.Objects; - import javax.inject.Inject; import org.gradle.api.GradleException; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; import org.gradle.api.internal.plugins.DslObject; import org.gradle.api.plugins.GroovyBasePlugin; -import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.GroovySourceDirectorySet; import org.gradle.api.tasks.GroovySourceSet; -import org.gradle.api.tasks.SourceSet; +import org.gradle.util.GradleVersion; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; -import com.diffplug.spotless.extra.groovy.GrEclipseFormatterStep; import com.diffplug.spotless.generic.LicenseHeaderStep; -import com.diffplug.spotless.java.ImportOrderStep; -public class GroovyExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { +public class GroovyExtension extends BaseGroovyExtension implements HasBuiltinDelimiterForLicense, JvmLang { + private boolean excludeJava = false; static final String NAME = "groovy"; @Inject @@ -43,8 +35,6 @@ public GroovyExtension(SpotlessExtension spotless) { super(spotless); } - boolean excludeJava = false; - /** Excludes .java files, to focus on only .groovy files. */ public void excludeJava() { excludeJava(true); @@ -65,62 +55,32 @@ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) { return licenseHeaderFile(licenseHeaderFile, JavaExtension.LICENSE_HEADER_DELIMITER); } - public void importOrder(String... importOrder) { - addStep(ImportOrderStep.forGroovy().createFrom(importOrder)); - } - - public void importOrderFile(Object importOrderFile) { - Objects.requireNonNull(importOrderFile); - addStep(ImportOrderStep.forGroovy().createFrom(getProject().file(importOrderFile))); - } - - public GrEclipseConfig greclipse() { - return greclipse(GrEclipseFormatterStep.defaultVersion()); - } - - public GrEclipseConfig greclipse(String version) { - return new GrEclipseConfig(version, this); - } - - public static class GrEclipseConfig { - private final EclipseBasedStepBuilder builder; - private final FormatExtension extension; - - GrEclipseConfig(String version, FormatExtension extension) { - this.extension = extension; - builder = GrEclipseFormatterStep.createBuilder(extension.provisioner()); - builder.setVersion(version); - extension.addStep(builder.build()); - } - - public void configFile(Object... configFiles) { - requireElementsNonNull(configFiles); - Project project = extension.getProject(); - builder.setPreferences(project.files(configFiles).getFiles()); - extension.replaceStep(builder.build()); - } - } - /** If the user hasn't specified the files yet, we'll assume he/she means all of the groovy files. */ @Override protected void setupTask(SpotlessTask task) { if (target == null) { - JavaPluginConvention convention = getProject().getConvention().getPlugin(JavaPluginConvention.class); - if (convention == null || !getProject().getPlugins().hasPlugin(GroovyBasePlugin.class)) { - throw new GradleException("You must apply the groovy plugin before the spotless plugin if you are using the groovy extension."); - } - //Add all Groovy files (may contain Java files as well) - - FileCollection union = getProject().files(); - for (SourceSet sourceSet : convention.getSourceSets()) { - GroovySourceSet groovySourceSet = new DslObject(sourceSet).getConvention().getPlugin(GroovySourceSet.class); - if (excludeJava) { - union = union.plus(groovySourceSet.getAllGroovy()); - } else { - union = union.plus(groovySourceSet.getGroovy()); - } + final String message = "You must either specify 'target' manually or apply the 'groovy' plugin."; + if (!getProject().getPlugins().hasPlugin(GroovyBasePlugin.class)) { + throw new GradleException(message); } - target = union; + target = getSources(getProject(), + message, + sourceSet -> { + if (GradleVersion.current().compareTo(GradleVersion.version(SpotlessPlugin.VER_GRADLE_javaPluginExtension)) >= 0) { + return sourceSet.getExtensions().getByType(GroovySourceDirectorySet.class); + } else { + final GroovySourceSet groovySourceSet = new DslObject(sourceSet).getConvention().getPlugin(GroovySourceSet.class); + return groovySourceSet.getGroovy(); + } + }, + file -> { + final String name = file.getName(); + if (excludeJava) { + return name.endsWith(".groovy"); + } else { + return name.endsWith(".groovy") || name.endsWith(".java"); + } + }); } else if (excludeJava) { throw new IllegalArgumentException("'excludeJava' is not supported in combination with a custom 'target'."); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java index 30cb75a2a3..120f99f27a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GroovyGradleExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,9 @@ */ package com.diffplug.gradle.spotless; -import java.util.Objects; - import javax.inject.Inject; -import com.diffplug.spotless.extra.groovy.GrEclipseFormatterStep; -import com.diffplug.spotless.java.ImportOrderStep; - -public class GroovyGradleExtension extends FormatExtension { +public class GroovyGradleExtension extends BaseGroovyExtension { private static final String GRADLE_FILE_EXTENSION = "*.gradle"; static final String NAME = "groovyGradle"; @@ -31,23 +26,6 @@ public GroovyGradleExtension(SpotlessExtension spotless) { super(spotless); } - public void importOrder(String... importOrder) { - addStep(ImportOrderStep.forGroovy().createFrom(importOrder)); - } - - public void importOrderFile(Object importOrderFile) { - Objects.requireNonNull(importOrderFile); - addStep(ImportOrderStep.forGroovy().createFrom(getProject().file(importOrderFile))); - } - - public GroovyExtension.GrEclipseConfig greclipse() { - return new GroovyExtension.GrEclipseConfig(GrEclipseFormatterStep.defaultVersion(), this); - } - - public GroovyExtension.GrEclipseConfig greclipse(String version) { - return new GroovyExtension.GrEclipseConfig(version, this); - } - @Override protected void setupTask(SpotlessTask task) { if (target == null) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java index 8d24e2230e..8b7253fd8c 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,34 @@ import java.io.IOException; import java.nio.file.Files; +import javax.annotation.Nullable; + +import org.gradle.api.Project; + import com.diffplug.common.base.Errors; import com.diffplug.common.io.ByteStreams; +import com.diffplug.spotless.DirtyState; import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.PaddedCell; +import com.diffplug.spotless.NoLambda; class IdeHook { + static class State extends NoLambda.EqualityBasedOnSerialization { + final @Nullable String path; + final boolean useStdIn; + final boolean useStdOut; + + State(Project project) { + path = (String) project.findProperty(PROPERTY); + if (path != null) { + useStdIn = project.hasProperty(USE_STD_IN); + useStdOut = project.hasProperty(USE_STD_OUT); + } else { + useStdIn = false; + useStdOut = false; + } + } + } + final static String PROPERTY = "spotlessIdeHook"; final static String USE_STD_IN = "spotlessIdeHookUseStdIn"; final static String USE_STD_OUT = "spotlessIdeHookUseStdOut"; @@ -33,9 +55,8 @@ private static void dumpIsClean() { System.err.println("IS CLEAN"); } - static void performHook(SpotlessTaskImpl spotlessTask) { - String path = (String) spotlessTask.getProject().property(PROPERTY); - File file = new File(path); + static void performHook(SpotlessTaskImpl spotlessTask, IdeHook.State state) { + File file = new File(state.path); if (!file.isAbsolute()) { System.err.println("Argument passed to " + PROPERTY + " must be an absolute path"); return; @@ -50,12 +71,12 @@ static void performHook(SpotlessTaskImpl spotlessTask) { } } byte[] bytes; - if (spotlessTask.getProject().hasProperty(USE_STD_IN)) { + if (state.useStdIn) { bytes = ByteStreams.toByteArray(System.in); } else { bytes = Files.readAllBytes(file.toPath()); } - PaddedCell.DirtyState dirty = PaddedCell.calculateDirtyState(formatter, file, bytes); + DirtyState dirty = DirtyState.of(formatter, file, bytes); if (dirty.isClean()) { dumpIsClean(); } else if (dirty.didNotConverge()) { @@ -63,7 +84,7 @@ static void performHook(SpotlessTaskImpl spotlessTask) { System.err.println("Run 'spotlessDiagnose' for details https://github.com/diffplug/spotless/blob/main/PADDEDCELL.md"); } else { System.err.println("IS DIRTY"); - if (spotlessTask.getProject().hasProperty(USE_STD_OUT)) { + if (state.useStdOut) { dirty.writeCanonicalTo(System.out); } else { dirty.writeCanonicalTo(file); 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 479d2ffd12..b7d3c84304 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,26 +18,31 @@ import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.inject.Inject; -import org.gradle.api.GradleException; import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; -import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; import com.diffplug.spotless.generic.LicenseHeaderStep; +import com.diffplug.spotless.java.CleanthatJavaStep; +import com.diffplug.spotless.java.FormatAnnotationsStep; import com.diffplug.spotless.java.GoogleJavaFormatStep; import com.diffplug.spotless.java.ImportOrderStep; import com.diffplug.spotless.java.PalantirJavaFormatStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; -public class JavaExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { +public class JavaExtension extends FormatExtension implements HasBuiltinDelimiterForLicense, JvmLang { static final String NAME = "java"; @Inject @@ -45,9 +50,7 @@ public JavaExtension(SpotlessExtension spotless) { super(spotless); } - // If this constant changes, don't forget to change the similarly-named one in - // testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java as well - static final String LICENSE_HEADER_DELIMITER = "package "; + static final String LICENSE_HEADER_DELIMITER = LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; @Override public LicenseHeaderConfig licenseHeader(String licenseHeader) { @@ -73,6 +76,9 @@ public class ImportOrderConfig { final File importOrderFile; boolean wildcardsLast = false; + boolean semanticSort = false; + Set treatAsPackage = Set.of(); + Set treatAsClass = Set.of(); ImportOrderConfig(String[] importOrder) { this.importOrder = importOrder; @@ -97,18 +103,52 @@ public ImportOrderConfig wildcardsLast(boolean wildcardsLast) { return this; } + public ImportOrderConfig semanticSort() { + return semanticSort(true); + } + + public ImportOrderConfig semanticSort(boolean semanticSort) { + this.semanticSort = semanticSort; + replaceStep(createStep()); + return this; + } + + public ImportOrderConfig treatAsPackage(String... treatAsPackage) { + return treatAsPackage(Arrays.asList(treatAsPackage)); + } + + public ImportOrderConfig treatAsPackage(Collection treatAsPackage) { + this.treatAsPackage = new HashSet<>(treatAsPackage); + replaceStep(createStep()); + return this; + } + + public ImportOrderConfig treatAsClass(String... treatAsClass) { + return treatAsClass(Arrays.asList(treatAsClass)); + } + + public ImportOrderConfig treatAsClass(Collection treatAsClass) { + this.treatAsClass = new HashSet<>(treatAsClass); + replaceStep(createStep()); + return this; + } + private FormatterStep createStep() { ImportOrderStep importOrderStep = ImportOrderStep.forJava(); return importOrderFile != null - ? importOrderStep.createFrom(wildcardsLast, getProject().file(importOrderFile)) - : importOrderStep.createFrom(wildcardsLast, importOrder); + ? importOrderStep.createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, getProject().file(importOrderFile)) + : importOrderStep.createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, importOrder); } } /** Removes any unused imports. */ public void removeUnusedImports() { - addStep(RemoveUnusedImportsStep.create(provisioner())); + addStep(RemoveUnusedImportsStep.create(RemoveUnusedImportsStep.defaultFormatter(), provisioner())); + } + + public void removeUnusedImports(String formatter) { + addStep(RemoveUnusedImportsStep.create(formatter, provisioner())); } /** Uses the google-java-format jar to format source code. */ @@ -118,9 +158,9 @@ public GoogleJavaFormatConfig googleJavaFormat() { /** * Uses the given version of google-java-format to format source code. - * + *

* Limited to published versions. See issue #33 - * for an workaround for using snapshot versions. + * for a workaround for using snapshot versions. */ public GoogleJavaFormatConfig googleJavaFormat(String version) { Objects.requireNonNull(version); @@ -132,6 +172,8 @@ public class GoogleJavaFormatConfig { String groupArtifact; String style; boolean reflowLongStrings; + boolean reorderImports; + boolean formatJavadoc = true; GoogleJavaFormatConfig(String version) { this.version = Objects.requireNonNull(version); @@ -166,13 +208,31 @@ public GoogleJavaFormatConfig reflowLongStrings(boolean reflowLongStrings) { return this; } + public GoogleJavaFormatConfig reorderImports(boolean reorderImports) { + this.reorderImports = reorderImports; + replaceStep(createStep()); + return this; + } + + public GoogleJavaFormatConfig skipJavadocFormatting() { + return formatJavadoc(false); + } + + public GoogleJavaFormatConfig formatJavadoc(boolean formatJavadoc) { + this.formatJavadoc = formatJavadoc; + replaceStep(createStep()); + return this; + } + private FormatterStep createStep() { return GoogleJavaFormatStep.create( groupArtifact, version, style, provisioner(), - reflowLongStrings); + reflowLongStrings, + reorderImports, + formatJavadoc); } } @@ -183,9 +243,9 @@ public PalantirJavaFormatConfig palantirJavaFormat() { /** * Uses the given version of palantir-java-format to format source code. - * + *

* Limited to published versions. See issue #33 - * for an workaround for using snapshot versions. + * for a workaround for using snapshot versions. */ public PalantirJavaFormatConfig palantirJavaFormat(String version) { Objects.requireNonNull(version); @@ -194,19 +254,34 @@ public PalantirJavaFormatConfig palantirJavaFormat(String version) { public class PalantirJavaFormatConfig { final String version; + String style; + boolean formatJavadoc; PalantirJavaFormatConfig(String version) { this.version = Objects.requireNonNull(version); + this.style = PalantirJavaFormatStep.defaultStyle(); addStep(createStep()); } + public PalantirJavaFormatConfig style(String style) { + this.style = Objects.requireNonNull(style); + replaceStep(createStep()); + return this; + } + + public PalantirJavaFormatConfig formatJavadoc(boolean formatJavadoc) { + this.formatJavadoc = formatJavadoc; + replaceStep(createStep()); + return this; + } + private FormatterStep createStep() { - return PalantirJavaFormatStep.create(version, provisioner()); + return PalantirJavaFormatStep.create(version, style, formatJavadoc, provisioner()); } } public EclipseConfig eclipse() { - return new EclipseConfig(EclipseJdtFormatterStep.defaultVersion()); + return eclipse(EclipseJdtFormatterStep.defaultVersion()); } public EclipseConfig eclipse(String version) { @@ -214,7 +289,7 @@ public EclipseConfig eclipse(String version) { } public class EclipseConfig { - private final EclipseBasedStepBuilder builder; + private final EclipseJdtFormatterStep.Builder builder; EclipseConfig(String version) { builder = EclipseJdtFormatterStep.createBuilder(provisioner()); @@ -222,28 +297,190 @@ public class EclipseConfig { addStep(builder.build()); } - public void configFile(Object... configFiles) { + public EclipseConfig configFile(Object... configFiles) { requireElementsNonNull(configFiles); Project project = getProject(); builder.setPreferences(project.files(configFiles).getFiles()); replaceStep(builder.build()); + return this; + } + + public EclipseConfig configProperties(String... configs) { + requireElementsNonNull(configs); + builder.setPropertyPreferences(List.of(configs)); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig sortMembersDoNotSortFields(boolean doNotSortFields) { + builder.sortMembersDoNotSortFields(doNotSortFields); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig sortMembersEnabled(boolean enabled) { + builder.sortMembersEnabled(enabled); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig sortMembersOrder(String order) { + requireElementsNonNull(order); + builder.sortMembersOrder(order); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig sortMembersVisibilityOrder(String order) { + requireElementsNonNull(order); + builder.sortMembersVisibilityOrder(order); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig sortMembersVisibilityOrderEnabled(boolean enabled) { + builder.sortMembersVisibilityOrderEnabled(enabled); + replaceStep(builder.build()); + return this; + } + + public EclipseConfig withP2Mirrors(Map mirrors) { + builder.setP2Mirrors(mirrors); + replaceStep(builder.build()); + return this; } } + /** Removes newlines between type annotations and types. */ + public FormatAnnotationsConfig formatAnnotations() { + return new FormatAnnotationsConfig(); + } + + public class FormatAnnotationsConfig { + /** Annotations in addition to those in the default list. */ + final List addedTypeAnnotations = new ArrayList<>(); + /** Annotations that the user doesn't want treated as type annotations. */ + final List removedTypeAnnotations = new ArrayList<>(); + + FormatAnnotationsConfig() { + addStep(createStep()); + } + + public FormatAnnotationsConfig addTypeAnnotation(String simpleName) { + Objects.requireNonNull(simpleName); + addedTypeAnnotations.add(simpleName); + replaceStep(createStep()); + return this; + } + + public FormatAnnotationsConfig removeTypeAnnotation(String simpleName) { + Objects.requireNonNull(simpleName); + removedTypeAnnotations.add(simpleName); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return FormatAnnotationsStep.create( + addedTypeAnnotations, + removedTypeAnnotations); + } + } + + /** Apply CleanThat refactoring rules. */ + public CleanthatJavaConfig cleanthat() { + return new CleanthatJavaConfig(); + } + + public class CleanthatJavaConfig { + private String groupArtifact = CleanthatJavaStep.defaultGroupArtifact(); + + private String version = CleanthatJavaStep.defaultVersion(); + + private String sourceJdk = CleanthatJavaStep.defaultSourceJdk(); + + private List mutators = new ArrayList<>(CleanthatJavaStep.defaultMutators()); + + private List excludedMutators = new ArrayList<>(CleanthatJavaStep.defaultExcludedMutators()); + + private boolean includeDraft = CleanthatJavaStep.defaultIncludeDraft(); + + CleanthatJavaConfig() { + addStep(createStep()); + } + + public CleanthatJavaConfig groupArtifact(String groupArtifact) { + Objects.requireNonNull(groupArtifact); + this.groupArtifact = groupArtifact; + replaceStep(createStep()); + return this; + } + + public CleanthatJavaConfig version(String version) { + Objects.requireNonNull(version); + this.version = version; + replaceStep(createStep()); + return this; + } + + public CleanthatJavaConfig sourceCompatibility(String jdkVersion) { + Objects.requireNonNull(jdkVersion); + this.sourceJdk = jdkVersion; + replaceStep(createStep()); + return this; + } + + // Especially useful to clear default mutators + public CleanthatJavaConfig clearMutators() { + this.mutators.clear(); + replaceStep(createStep()); + return this; + } + + // An id of a mutator (see IMutator.getIds()) or + // tThe fully qualified name of a class implementing eu.solven.cleanthat.engine.java.refactorer.meta.IMutator + public CleanthatJavaConfig addMutator(String mutator) { + this.mutators.add(mutator); + replaceStep(createStep()); + return this; + } + + public CleanthatJavaConfig addMutators(Collection mutators) { + this.mutators.addAll(mutators); + replaceStep(createStep()); + return this; + } + + // useful to exclude a mutator amongst the default list of mutators + public CleanthatJavaConfig excludeMutator(String mutator) { + this.excludedMutators.add(mutator); + replaceStep(createStep()); + return this; + } + + public CleanthatJavaConfig includeDraft(boolean includeDraft) { + this.includeDraft = includeDraft; + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return CleanthatJavaStep.create( + groupArtifact, + version, + sourceJdk, mutators, excludedMutators, includeDraft, provisioner()); + } + } + /** If the user hasn't specified the files yet, we'll assume he/she means all of the java files. */ @Override protected void setupTask(SpotlessTask task) { if (target == null) { - JavaPluginConvention javaPlugin = getProject().getConvention().findPlugin(JavaPluginConvention.class); - if (javaPlugin == null) { - throw new GradleException("You must either specify 'target' manually or apply the 'java' plugin."); - } - FileCollection union = getProject().files(); - for (SourceSet sourceSet : javaPlugin.getSourceSets()) { - union = union.plus(sourceSet.getAllJava()); - } - target = union; + target = getSources(getProject(), + "You must either specify 'target' manually or apply the 'java' plugin.", + SourceSet::getAllJava, + file -> file.getName().endsWith(".java")); } steps.replaceAll(step -> { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java new file mode 100644 index 0000000000..5532045bc2 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java @@ -0,0 +1,228 @@ +/* + * Copyright 2016-2024 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 java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.gradle.api.Project; + +import com.diffplug.common.collect.ImmutableList; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.npm.EslintConfig; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.NpmPathResolver; +import com.diffplug.spotless.npm.PrettierFormatterStep; + +public class JavascriptExtension extends FormatExtension { + + static final String NAME = "javascript"; + + @Inject + public JavascriptExtension(SpotlessExtension spotless) { + super(spotless); + } + + public JavascriptEslintConfig eslint() { + return eslint(EslintFormatterStep.defaultDevDependenciesForTypescript()); + } + + public JavascriptEslintConfig eslint(String version) { + return eslint(EslintFormatterStep.defaultDevDependenciesTypescriptWithEslint(version)); + } + + public JavascriptEslintConfig eslint(Map devDependencies) { + JavascriptEslintConfig eslint = new JavascriptEslintConfig(devDependencies); + addStep(eslint.createStep()); + return eslint; + } + + public static abstract class EslintBaseConfig> + extends NpmStepConfig> { + Map devDependencies = new LinkedHashMap<>(); + + @Nullable + Object configFilePath = null; + + @Nullable + String configJs = null; + + public EslintBaseConfig(Project project, Consumer replaceStep, + Map devDependencies) { + super(project, replaceStep); + this.devDependencies.putAll(requireNonNull(devDependencies)); + } + + @SuppressWarnings("unchecked") + protected T devDependencies(Map devDependencies) { + this.devDependencies.putAll(devDependencies); + replaceStep(); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T configJs(String configJs) { + this.configJs = requireNonNull(configJs); + replaceStep(); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T configFile(Object configFilePath) { + this.configFilePath = requireNonNull(configFilePath); + replaceStep(); + return (T) this; + } + } + + public class JavascriptEslintConfig extends EslintBaseConfig { + + public JavascriptEslintConfig(Map devDependencies) { + super(getProject(), JavascriptExtension.this::replaceStep, devDependencies); + } + + public FormatterStep createStep() { + final Project project = getProject(); + + return EslintFormatterStep.create(devDependencies, provisioner(), project.getProjectDir(), + project.getLayout().getBuildDirectory().getAsFile().get(), npmModulesCacheOrNull(), + new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), + Arrays.asList(project.getProjectDir(), project.getRootDir())), + eslintConfig()); + } + + protected EslintConfig eslintConfig() { + return new EslintConfig(configFilePath != null ? getProject().file(configFilePath) : null, configJs); + } + } + + /** Uses the default version of prettier. */ + @Override + public PrettierConfig prettier() { + return prettier(PrettierFormatterStep.defaultDevDependencies()); + } + + /** Uses the specified version of prettier. */ + @Override + public PrettierConfig prettier(String version) { + return prettier(PrettierFormatterStep.defaultDevDependenciesWithPrettier(version)); + } + + /** Uses exactly the npm packages specified in the map. */ + @Override + public PrettierConfig prettier(Map devDependencies) { + PrettierConfig prettierConfig = new JavascriptPrettierConfig(devDependencies); + addStep(prettierConfig.createStep()); + return prettierConfig; + } + + /** + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + */ + public BiomeJs biome() { + return biome(null); + } + + /** Downloads the given Biome version from the network. */ + public BiomeJs biome(String version) { + var biomeConfig = new BiomeJs(version); + addStep(biomeConfig.createStep()); + return biomeConfig; + } + + private static final String DEFAULT_PRETTIER_JS_PARSER = "babel"; + private static final ImmutableList PRETTIER_JS_PARSERS = ImmutableList.of(DEFAULT_PRETTIER_JS_PARSER, + "babel-flow", "flow"); + + /** + * Biome formatter step for JavaScript. + */ + public class BiomeJs extends BiomeStepConfig { + /** + * Creates a new Biome formatter step config for formatting JavaScript files. + * Unless overwritten, the given Biome version is downloaded from the network. + * + * @param version Biome version to use. + */ + public BiomeJs(String version) { + super(getProject(), JavascriptExtension.this::replaceStep, BiomeFlavor.BIOME, version); + } + + @Override + protected String getLanguage() { + return "js?"; + } + + @Override + protected BiomeJs getThis() { + return this; + } + } + + /** + * Overrides the parser to be set to a js parser. + */ + public class JavascriptPrettierConfig extends PrettierConfig { + + JavascriptPrettierConfig(Map devDependencies) { + super(devDependencies); + } + + @Override + protected FormatterStep createStep() { + fixParserToJavascript(); + return super.createStep(); + } + + private void fixParserToJavascript() { + if (this.prettierConfig == null) { + this.prettierConfig = Collections.singletonMap("parser", DEFAULT_PRETTIER_JS_PARSER); + } else { + final Object currentParser = this.prettierConfig.get("parser"); + if (PRETTIER_JS_PARSERS.contains(String.valueOf(currentParser))) { + getProject().getLogger().debug("Already javascript parser set, not overriding."); + } else { + this.prettierConfig.put("parser", DEFAULT_PRETTIER_JS_PARSER); + if (currentParser != null) { + getProject().getLogger().warn( + "Overriding parser option to '{}'. (Was set to '{}'.) Set it to another js parser if you have problems with '{}'.", + DEFAULT_PRETTIER_JS_PARSER, currentParser, DEFAULT_PRETTIER_JS_PARSER); + } + } + + } + } + } + + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + throw noDefaultTargetException(); + } + super.setupTask(task); + } +} 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 e15d395f3b..47db97b2e6 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-2021 DiffPlug + * Copyright 2016-2024 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,23 @@ */ package com.diffplug.gradle.spotless; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import javax.inject.Inject; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.json.JacksonJsonConfig; +import com.diffplug.spotless.json.JacksonJsonStep; +import com.diffplug.spotless.json.JsonPatchStep; import com.diffplug.spotless.json.JsonSimpleStep; +import com.diffplug.spotless.json.gson.GsonStep; 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 @@ -41,6 +51,38 @@ public SimpleConfig simple() { return new SimpleConfig(DEFAULT_INDENTATION); } + public GsonConfig gson() { + return new GsonConfig(); + } + + public JacksonJsonGradleConfig jackson() { + return new JacksonJsonGradleConfig(this); + } + + /** + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + */ + public BiomeJson biome() { + return biome(null); + } + + /** Downloads the given Biome version from the network. */ + public BiomeJson biome(String version) { + var biomeConfig = new BiomeJson(version); + addStep(biomeConfig.createStep()); + return biomeConfig; + } + + public JsonPatchConfig jsonPatch(List> patch) { + return new JsonPatchConfig(patch); + } + + public JsonPatchConfig jsonPatch(String zjsonPatchVersion, List> patch) { + return new JsonPatchConfig(zjsonPatchVersion, patch); + } + public class SimpleConfig { private int indent; @@ -59,4 +101,133 @@ private FormatterStep createStep() { } } + public class GsonConfig { + private int indentSpaces; + private boolean sortByKeys; + private boolean escapeHtml; + private String version; + + public GsonConfig() { + this.indentSpaces = DEFAULT_INDENTATION; + this.sortByKeys = false; + this.escapeHtml = false; + this.version = GsonStep.DEFAULT_VERSION; + addStep(createStep()); + } + + public GsonConfig indentWithSpaces(int indentSpaces) { + this.indentSpaces = indentSpaces; + replaceStep(createStep()); + return this; + } + + public GsonConfig sortByKeys() { + this.sortByKeys = true; + replaceStep(createStep()); + return this; + } + + public GsonConfig escapeHtml() { + this.escapeHtml = true; + replaceStep(createStep()); + return this; + } + + public GsonConfig version(String version) { + this.version = version; + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return GsonStep.create( + new com.diffplug.spotless.json.gson.GsonConfig(sortByKeys, escapeHtml, indentSpaces, version), + provisioner()); + } + } + + public static class JacksonJsonGradleConfig extends AJacksonGradleConfig { + protected JacksonJsonConfig jacksonConfig; + + public JacksonJsonGradleConfig(JacksonJsonConfig jacksonConfig, FormatExtension formatExtension) { + super(jacksonConfig, formatExtension); + this.jacksonConfig = jacksonConfig; + + formatExtension.addStep(createStep()); + } + + public JacksonJsonGradleConfig(FormatExtension formatExtension) { + this(new JacksonJsonConfig(), formatExtension); + } + + /** + * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature + */ + public JacksonJsonGradleConfig jsonFeature(String feature, boolean toggle) { + this.jacksonConfig.appendJsonFeatureToToggle(Collections.singletonMap(feature, toggle)); + formatExtension.replaceStep(createStep()); + return this; + } + + @Override + public JacksonJsonGradleConfig self() { + return this; + } + + // 'final' as it is called in the constructor + @Override + protected final FormatterStep createStep() { + return JacksonJsonStep.create(jacksonConfig, version, formatExtension.provisioner()); + } + } + + /** + * Biome formatter step for JSON. + */ + public class BiomeJson extends BiomeStepConfig { + /** + * Creates a new Biome formatter step config for formatting JSON files. Unless + * overwritten, the given Biome version is downloaded from the network. + * + * @param version Biome version to use. + */ + public BiomeJson(String version) { + super(getProject(), JsonExtension.this::replaceStep, BiomeFlavor.BIOME, version); + } + + @Override + protected String getLanguage() { + return "json"; + } + + @Override + protected BiomeJson getThis() { + return this; + } + } + + public class JsonPatchConfig { + private String zjsonPatchVersion; + private List> patch; + + public JsonPatchConfig(List> patch) { + this(DEFAULT_ZJSONPATCH_VERSION, patch); + } + + public JsonPatchConfig(String zjsonPatchVersion, List> patch) { + this.zjsonPatchVersion = zjsonPatchVersion; + this.patch = patch; + addStep(createStep()); + } + + public JsonPatchConfig version(String zjsonPatchVersion) { + this.zjsonPatchVersion = zjsonPatchVersion; + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return JsonPatchStep.create(zjsonPatchVersion, patch, provisioner()); + } + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java new file mode 100644 index 0000000000..1a9a06b986 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLang.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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.File; +import java.util.function.Function; + +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.util.GradleVersion; + +interface JvmLang { + + default FileCollection getSources(Project project, String message, Function sourceSetSourceDirectory, Spec filterSpec) { + final SourceSetContainer sourceSets; + FileCollection union = project.files(); + if (GradleVersion.current().compareTo(GradleVersion.version(SpotlessPlugin.VER_GRADLE_javaPluginExtension)) >= 0) { + final JavaPluginExtension javaPluginExtension = project.getExtensions().findByType(JavaPluginExtension.class); + if (javaPluginExtension == null) { + throw new GradleException(message); + } + sourceSets = javaPluginExtension.getSourceSets(); + } else { + final JavaPluginConvention javaPluginConvention = project.getConvention().findPlugin(JavaPluginConvention.class); + if (javaPluginConvention == null) { + throw new GradleException(message); + } + sourceSets = javaPluginConvention.getSourceSets(); + } + for (SourceSet sourceSet : sourceSets) { + union = union.plus(sourceSetSourceDirectory.apply(sourceSet).filter(filterSpec)); + } + return union; + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLocalCache.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLocalCache.java deleted file mode 100644 index 9478e9e66f..0000000000 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JvmLocalCache.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2021 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.File; -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import org.gradle.api.GradleException; -import org.gradle.api.Task; - -import com.diffplug.spotless.FileSignature; - -class JvmLocalCache { - private static GradleException cacheIsStale() { - return new GradleException("Spotless JVM-local cache is stale. Regenerate the cache with\n" + - " " + (FileSignature.machineIsWin() ? "rmdir /q /s" : "rm -rf") + " .gradle/configuration-cache\n" + - "To make this workaround obsolete, please upvote https://github.com/diffplug/spotless/issues/987"); - } - - interface LiveCache { - T get(); - - void set(T value); - } - - static LiveCache createLive(Task task, String propertyName) { - return new LiveCacheKeyImpl(new InternalCacheKey(task.getProject().getProjectDir(), task.getPath(), propertyName)); - } - - static class LiveCacheKeyImpl implements LiveCache, Serializable { - InternalCacheKey internalKey; - - LiveCacheKeyImpl(InternalCacheKey internalKey) { - this.internalKey = internalKey; - } - - @Override - public void set(T value) { - daemonState.put(internalKey, value); - } - - @Override - public T get() { - Object value = daemonState.get(internalKey); - if (value == null) { - // TODO: throw TriggerConfigurationException(); (see https://github.com/diffplug/spotless/issues/987) - throw cacheIsStale(); - } else { - return (T) value; - } - } - } - - private static Map daemonState = Collections.synchronizedMap(new HashMap<>()); - - private static class InternalCacheKey implements Serializable { - private File projectDir; - private String taskPath; - private String propertyName; - - InternalCacheKey(File projectDir, String taskPath, String keyName) { - this.projectDir = projectDir; - this.taskPath = taskPath; - this.propertyName = keyName; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - InternalCacheKey that = (InternalCacheKey) o; - return projectDir.equals(that.projectDir) && taskPath.equals(that.taskPath) && propertyName.equals(that.propertyName); - } - - @Override - public int hashCode() { - return Objects.hash(projectDir, taskPath, propertyName); - } - } -} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java index d88b4aac7f..21ccb8dd0f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,26 +17,11 @@ import static com.diffplug.spotless.kotlin.KotlinConstants.LICENSE_HEADER_DELIMITER; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; - import javax.inject.Inject; -import org.gradle.api.GradleException; -import org.gradle.api.file.FileCollection; -import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; -import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.kotlin.DiktatStep; -import com.diffplug.spotless.kotlin.KtLintStep; -import com.diffplug.spotless.kotlin.KtfmtStep; -import com.diffplug.spotless.kotlin.KtfmtStep.Style; - -public class KotlinExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { +public class KotlinExtension extends BaseKotlinExtension implements HasBuiltinDelimiterForLicense, JvmLang { static final String NAME = "kotlin"; @Inject @@ -54,137 +39,22 @@ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) { return licenseHeaderFile(licenseHeaderFile, LICENSE_HEADER_DELIMITER); } - /** Adds the specified version of ktlint. */ - public KotlinFormatExtension ktlint(String version) { - Objects.requireNonNull(version); - return new KotlinFormatExtension(version, Collections.emptyMap()); - } - - public KotlinFormatExtension ktlint() { - return ktlint(KtLintStep.defaultVersion()); - } - - public class KotlinFormatExtension { - - private final String version; - private Map userData; - - KotlinFormatExtension(String version, Map config) { - this.version = version; - this.userData = config; - addStep(createStep()); - } - - public void userData(Map userData) { - // Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized - // representation. - this.userData = userData; - replaceStep(createStep()); - } - - private FormatterStep createStep() { - return KtLintStep.create(version, provisioner(), userData); - } - } - - /** 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 - * option to format source code. - */ - public KtfmtConfig ktfmt(String version) { - Objects.requireNonNull(version); - return new KtfmtConfig(version); - } - - public class KtfmtConfig { - final String version; - Style style; - - KtfmtConfig(String version) { - this.version = Objects.requireNonNull(version); - this.style = Style.DEFAULT; - addStep(createStep()); - } - - public void dropboxStyle() { - style(Style.DROPBOX); - } - - public void googleStyle() { - style(Style.GOOGLE); - } - - public void kotlinlangStyle() { - style(Style.KOTLINLANG); - } - - public void style(Style style) { - this.style = style; - replaceStep(createStep()); - } - - private FormatterStep createStep() { - return KtfmtStep.create(version, provisioner(), style); - } - } - - /** Adds the specified version of diktat. */ - public DiktatFormatExtension diktat(String version) { - Objects.requireNonNull(version); - return new DiktatFormatExtension(version); - } - - public DiktatFormatExtension diktat() { - return diktat(DiktatStep.defaultVersionDiktat()); - } - - public class DiktatFormatExtension { - - private final String version; - private FileSignature config; - - DiktatFormatExtension(String version) { - this.version = version; - addStep(createStep()); - } - - public DiktatFormatExtension configFile(Object file) throws IOException { - // Specify the path to the configuration file - if (file == null) { - this.config = null; - } else { - this.config = FileSignature.signAsList(getProject().file(file)); - } - replaceStep(createStep()); - return this; - } - - private FormatterStep createStep() { - return DiktatStep.create(version, provisioner(), config); - } + @Override + protected boolean isScript() { + return false; } /** If the user hasn't specified the files yet, we'll assume he/she means all of the kotlin files. */ @Override protected void setupTask(SpotlessTask task) { if (target == null) { - JavaPluginConvention javaPlugin = getProject().getConvention().findPlugin(JavaPluginConvention.class); - if (javaPlugin == null) { - throw new GradleException("You must either specify 'target' manually or apply a kotlin plugin."); - } - FileCollection union = getProject().files(); - for (SourceSet sourceSet : javaPlugin.getSourceSets()) { - union = union.plus(sourceSet.getAllSource().filter(file -> { - String name = file.getName(); - return name.endsWith(".kt") || name.endsWith(".kts"); - })); - } - target = union; + target = getSources(getProject(), + "You must either specify 'target' manually or apply a kotlin plugin.", + SourceSet::getAllSource, + file -> { + final String name = file.getName(); + return name.endsWith(".kt") || name.endsWith(".kts"); + }); } super.setupTask(task); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java index 5ea3c29594..5cf2847d62 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,22 +15,9 @@ */ package com.diffplug.gradle.spotless; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; - import javax.inject.Inject; -import com.diffplug.common.collect.ImmutableSortedMap; -import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.kotlin.DiktatStep; -import com.diffplug.spotless.kotlin.KtLintStep; -import com.diffplug.spotless.kotlin.KtfmtStep; -import com.diffplug.spotless.kotlin.KtfmtStep.Style; - -public class KotlinGradleExtension extends FormatExtension { +public class KotlinGradleExtension extends BaseKotlinExtension { private static final String GRADLE_KOTLIN_DSL_FILE_EXTENSION = "*.gradle.kts"; static final String NAME = "kotlinGradle"; @@ -40,119 +27,9 @@ public KotlinGradleExtension(SpotlessExtension spotless) { super(spotless); } - /** Adds the specified version of ktlint. */ - public KotlinFormatExtension ktlint(String version) { - Objects.requireNonNull(version, "version"); - return new KotlinFormatExtension(version, Collections.emptyMap()); - } - - public KotlinFormatExtension ktlint() { - return ktlint(KtLintStep.defaultVersion()); - } - - public class KotlinFormatExtension { - - private final String version; - private Map userData; - - KotlinFormatExtension(String version, Map config) { - this.version = version; - this.userData = config; - addStep(createStep()); - } - - public void userData(Map userData) { - // Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized - // representation. - this.userData = ImmutableSortedMap.copyOf(userData); - replaceStep(createStep()); - } - - private FormatterStep createStep() { - return KtLintStep.createForScript(version, provisioner(), userData); - } - } - - /** Uses the ktfmt jar to format source code. */ - public KtfmtConfig ktfmt() { - return ktfmt(KtfmtStep.defaultVersion()); - } - - /** - * Uses the given version of ktfmt to format source - * code. - */ - public KtfmtConfig ktfmt(String version) { - Objects.requireNonNull(version); - return new KtfmtConfig(version); - } - - public class KtfmtConfig { - final String version; - Style style; - - KtfmtConfig(String version) { - this.version = Objects.requireNonNull(version); - this.style = Style.DEFAULT; - addStep(createStep()); - } - - public void style(Style style) { - this.style = style; - replaceStep(createStep()); - } - - public void dropboxStyle() { - style(Style.DROPBOX); - } - - public void googleStyle() { - style(Style.GOOGLE); - } - - public void kotlinlangStyle() { - style(Style.KOTLINLANG); - } - - private FormatterStep createStep() { - return KtfmtStep.create(version, provisioner(), style); - } - } - - /** Adds the specified version of diktat. */ - public DiktatFormatExtension diktat(String version) { - Objects.requireNonNull(version, "version"); - return new DiktatFormatExtension(version); - } - - public DiktatFormatExtension diktat() { - return diktat(DiktatStep.defaultVersionDiktat()); - } - - public class DiktatFormatExtension { - - private final String version; - private FileSignature config; - - DiktatFormatExtension(String version) { - this.version = version; - addStep(createStep()); - } - - public DiktatFormatExtension configFile(Object file) throws IOException { - // Specify the path to the configuration file - if (file == null) { - this.config = null; - } else { - this.config = FileSignature.signAsList(getProject().file(file)); - } - replaceStep(createStep()); - return this; - } - - private FormatterStep createStep() { - return DiktatStep.createForScript(version, provisioner(), config); - } + @Override + protected boolean isScript() { + return true; } @Override diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PomExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PomExtension.java new file mode 100644 index 0000000000..4fd92c1f02 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PomExtension.java @@ -0,0 +1,169 @@ +/* + * Copyright 2024 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.util.Objects; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.pom.SortPomCfg; +import com.diffplug.spotless.pom.SortPomStep; + +public class PomExtension extends FormatExtension { + private static final String POM_FILE = "pom.xml"; + + static final String NAME = "pom"; + + @Inject + public PomExtension(SpotlessExtension spotless) { + super(spotless); + } + + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target = parseTarget(POM_FILE); + } + super.setupTask(task); + } + + public SortPomGradleConfig sortPom() { + return new SortPomGradleConfig(); + } + + public SortPomGradleConfig sortPom(String version) { + Objects.requireNonNull(version); + return new SortPomGradleConfig(version); + } + + public class SortPomGradleConfig { + private final SortPomCfg cfg = new SortPomCfg(); + + SortPomGradleConfig() { + addStep(createStep()); + } + + SortPomGradleConfig(String version) { + this(); + cfg.version = Objects.requireNonNull(version); + } + + public SortPomGradleConfig encoding(String encoding) { + cfg.encoding = encoding; + return this; + } + + public SortPomGradleConfig lineSeparator(String lineSeparator) { + cfg.lineSeparator = lineSeparator; + return this; + } + + public SortPomGradleConfig expandEmptyElements(boolean expandEmptyElements) { + cfg.expandEmptyElements = expandEmptyElements; + return this; + } + + public SortPomGradleConfig spaceBeforeCloseEmptyElement(boolean spaceBeforeCloseEmptyElement) { + cfg.spaceBeforeCloseEmptyElement = spaceBeforeCloseEmptyElement; + return this; + } + + public SortPomGradleConfig keepBlankLines(boolean keepBlankLines) { + cfg.keepBlankLines = keepBlankLines; + return this; + } + + public SortPomGradleConfig endWithNewline(boolean endWithNewline) { + cfg.endWithNewline = endWithNewline; + return this; + } + + public SortPomGradleConfig nrOfIndentSpace(int nrOfIndentSpace) { + cfg.nrOfIndentSpace = nrOfIndentSpace; + return this; + } + + public SortPomGradleConfig indentBlankLines(boolean indentBlankLines) { + cfg.indentBlankLines = indentBlankLines; + return this; + } + + public SortPomGradleConfig indentSchemaLocation(boolean indentSchemaLocation) { + cfg.indentSchemaLocation = indentSchemaLocation; + return this; + } + + public SortPomGradleConfig indentAttribute(String indentAttribute) { + cfg.indentAttribute = indentAttribute; + return this; + } + + public SortPomGradleConfig predefinedSortOrder(String predefinedSortOrder) { + cfg.predefinedSortOrder = predefinedSortOrder; + return this; + } + + public SortPomGradleConfig quiet(boolean quiet) { + cfg.quiet = quiet; + return this; + } + + public SortPomGradleConfig sortOrderFile(String sortOrderFile) { + cfg.sortOrderFile = sortOrderFile; + return this; + } + + public SortPomGradleConfig sortDependencies(String sortDependencies) { + cfg.sortDependencies = sortDependencies; + return this; + } + + public SortPomGradleConfig sortDependencyManagement(String sortDependencyManagement) { + cfg.sortDependencyManagement = sortDependencyManagement; + return this; + } + + public SortPomGradleConfig sortDependencyExclusions(String sortDependencyExclusions) { + cfg.sortDependencyExclusions = sortDependencyExclusions; + return this; + } + + public SortPomGradleConfig sortPlugins(String sortPlugins) { + cfg.sortPlugins = sortPlugins; + return this; + } + + public SortPomGradleConfig sortProperties(boolean sortProperties) { + cfg.sortProperties = sortProperties; + return this; + } + + public SortPomGradleConfig sortModules(boolean sortModules) { + cfg.sortModules = sortModules; + return this; + } + + public SortPomGradleConfig sortExecutions(boolean sortExecutions) { + cfg.sortExecutions = sortExecutions; + return this; + } + + private FormatterStep createStep() { + return SortPomStep.create(cfg, provisioner()); + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java new file mode 100644 index 0000000000..7b0a6bb30a --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022-2023 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 com.diffplug.spotless.protobuf.ProtobufConstants.LICENSE_HEADER_DELIMITER; + +import java.util.Objects; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.protobuf.BufStep; + +public class ProtobufExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { + static final String NAME = "protobuf"; + + @Inject + public ProtobufExtension(SpotlessExtension spotless) { + super(spotless); + } + + @Override + public LicenseHeaderConfig licenseHeader(String licenseHeader) { + return licenseHeader(licenseHeader, LICENSE_HEADER_DELIMITER); + } + + @Override + public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) { + return licenseHeaderFile(licenseHeaderFile, LICENSE_HEADER_DELIMITER); + } + + /** If the user hasn't specified files, assume all protobuf files should be checked. */ + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target = parseTarget("**/*.proto"); + } + super.setupTask(task); + } + + /** Adds the specified version of buf. */ + public BufFormatExtension buf(String version) { + Objects.requireNonNull(version); + return new BufFormatExtension(version); + } + + public BufFormatExtension buf() { + return buf(BufStep.defaultVersion()); + } + + public class BufFormatExtension { + BufStep step; + + BufFormatExtension(String version) { + this.step = BufStep.withVersion(version); + if (!steps.isEmpty()) { + throw new IllegalArgumentException("buf() must be the first step, move other steps after it. 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."); + } + addStep(createStep()); + } + + /** + * When used in conjunction with the {@code buf-gradle-plugin}, + * the {@code buf} executable can be resolved from its {@code bufTool} configuration: + * + *

+		 * {@code
+		 * spotless {
+		 *   protobuf {
+		 *     buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath())
+		 *   }
+		 * }
+		 * }
+		 * 
+ * + * Be sure to disable the {@code buf-gradle-plugin}'s execution of {@code buf format}: + * + *
+		 * {@code
+		 * buf {
+		 *   enforceFormat = false
+		 * }
+		 * }
+		 * 
+ */ + public BufFormatExtension pathToExe(String pathToExe) { + step = step.withPathToExe(pathToExe); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return step.create(); + } + } +} 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 a3fbdf4a81..33aded8bdd 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,14 @@ import javax.inject.Inject; import org.gradle.api.DefaultTask; -import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.services.BuildServiceRegistry; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; import org.gradle.build.event.BuildEventsListenerRegistry; +import org.gradle.work.DisableCachingByDefault; import com.diffplug.common.base.Preconditions; import com.diffplug.common.io.Files; @@ -38,7 +39,7 @@ /** * NOT AN END-USER TASK, DO NOT USE FOR ANYTHING! - * + *

* - When a user asks for a formatter, we need to download the jars for that formatter * - Gradle wants us to resolve all our dependencies in the root project - no new dependencies in subprojects * - So, whenever a SpotlessTask in a subproject gets configured, we call {@link #hookSubprojectTask(SpotlessTask)}, @@ -46,6 +47,7 @@ * - When this "registerDependencies" task does its up-to-date check, it queries the task execution graph to see which * SpotlessTasks are at risk of being executed, and causes them all to be evaluated safely in the root buildscript. */ +@DisableCachingByDefault(because = "This task coordinates the setup and execution of other tasks, and should not be cached") public abstract class RegisterDependenciesTask extends DefaultTask { static final String TASK_NAME = "spotlessInternalRegisterDependencies"; @@ -63,9 +65,10 @@ void setup() { Preconditions.checkArgument(getProject().getRootProject() == getProject(), "Can only be used on the root project"); String compositeBuildSuffix = getName().substring(TASK_NAME.length()); // see https://github.com/diffplug/spotless/pull/1001 BuildServiceRegistry buildServices = getProject().getGradle().getSharedServices(); - getTaskService().set(buildServices.registerIfAbsent("SpotlessTaskService" + compositeBuildSuffix, SpotlessTaskService.class, spec -> {})); - getBuildEventsListenerRegistry().onTaskCompletion(getTaskService()); - unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies"); + taskService = buildServices.registerIfAbsent("SpotlessTaskService" + compositeBuildSuffix, SpotlessTaskService.class, spec -> {}); + usesService(taskService); + getBuildEventsListenerRegistry().onTaskCompletion(taskService); + unitOutput = new File(getProject().getLayout().getBuildDirectory().getAsFile().get(), "tmp/spotless-register-dependencies"); } List steps = new ArrayList<>(); @@ -88,8 +91,13 @@ public void trivialFunction() throws IOException { Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8); } + // this field is stupid, but we need it, see https://github.com/diffplug/spotless/issues/1260 + private Provider taskService; + @Internal - abstract Property getTaskService(); + public Provider getTaskService() { + return taskService; + } @Inject protected abstract BuildEventsListenerRegistry getBuildEventsListenerRegistry(); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java index f6db64348d..1639f3473c 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ScalaExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,15 +21,12 @@ import javax.annotation.Nullable; import javax.inject.Inject; -import org.gradle.api.GradleException; -import org.gradle.api.file.FileCollection; -import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.scala.ScalaFmtStep; -public class ScalaExtension extends FormatExtension { +public class ScalaExtension extends FormatExtension implements JvmLang { static final String NAME = "scala"; @Inject @@ -48,6 +45,8 @@ public ScalaFmtConfig scalafmt(String version) { public class ScalaFmtConfig { final String version; @Nullable + String scalaMajorVersion; + @Nullable Object configFile; ScalaFmtConfig(String version) { @@ -55,14 +54,21 @@ public class ScalaFmtConfig { addStep(createStep()); } - public void configFile(Object configFile) { + public ScalaFmtConfig configFile(Object configFile) { this.configFile = Objects.requireNonNull(configFile); replaceStep(createStep()); + return this; + } + + public ScalaFmtConfig scalaMajorVersion(String scalaMajorVersion) { + this.scalaMajorVersion = Objects.requireNonNull(scalaMajorVersion); + replaceStep(createStep()); + return this; } private FormatterStep createStep() { File resolvedConfigFile = configFile == null ? null : getProject().file(configFile); - return ScalaFmtStep.create(version, provisioner(), resolvedConfigFile); + return ScalaFmtStep.create(version, scalaMajorVersion, provisioner(), resolvedConfigFile); } } @@ -70,18 +76,13 @@ private FormatterStep createStep() { @Override protected void setupTask(SpotlessTask task) { if (target == null) { - JavaPluginConvention javaPlugin = getProject().getConvention().findPlugin(JavaPluginConvention.class); - if (javaPlugin == null) { - throw new GradleException("You must either specify 'target' manually or apply the 'scala' plugin."); - } - FileCollection union = getProject().files(); - for (SourceSet sourceSet : javaPlugin.getSourceSets()) { - union = union.plus(sourceSet.getAllSource().filter(file -> { - String name = file.getName(); - return name.endsWith(".scala") || name.endsWith(".sc"); - })); - } - target = union; + target = getSources(getProject(), + "You must either specify 'target' manually or apply the 'scala' plugin.", + SourceSet::getAllSource, + file -> { + final String name = file.getName(); + return name.endsWith(".scala") || name.endsWith(".sc"); + }); } super.setupTask(task); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java new file mode 100644 index 0000000000..6a21f2ffb4 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ShellExtension.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 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.util.Objects; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.shell.ShfmtStep; + +public class ShellExtension extends FormatExtension { + private static final String SHELL_FILE_EXTENSION = "**/*.sh"; + + static final String NAME = "shell"; + + @Inject + public ShellExtension(SpotlessExtension spotless) { + super(spotless); + } + + /** If the user hasn't specified files, assume all shell files should be checked. */ + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target = parseTarget(SHELL_FILE_EXTENSION); + } + super.setupTask(task); + } + + /** Adds the specified version of shfmt. */ + public ShfmtExtension shfmt(String version) { + Objects.requireNonNull(version); + return new ShfmtExtension(version); + } + + /** Adds the specified version of shfmt. */ + public ShfmtExtension shfmt() { + return shfmt(ShfmtStep.defaultVersion()); + } + + public class ShfmtExtension { + ShfmtStep step; + + ShfmtExtension(String version) { + this.step = ShfmtStep.withVersion(version); + addStep(createStep()); + } + + public ShfmtExtension pathToExe(String pathToExe) { + step = step.withPathToExe(pathToExe); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return step.create(); + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java index a85195fe9f..c623669ab1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessApply.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,20 +20,24 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import org.gradle.api.GradleException; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.FileVisitDetails; import org.gradle.api.file.FileVisitor; import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +@DisableCachingByDefault(because = "not worth caching") public abstract class SpotlessApply extends SpotlessTaskService.ClientTask { @TaskAction public void performAction() { getTaskService().get().registerApplyAlreadyRan(this); - ConfigurableFileTree files = getConfigCacheWorkaround().fileTree().from(getSpotlessOutDirectory().get()); - if (files.isEmpty()) { + ConfigurableFileTree cleanFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessCleanDirectory().get()); + ConfigurableFileTree lintsFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessLintsDirectory().get()); + if (cleanFiles.isEmpty() && lintsFiles.isEmpty()) { getState().setDidWork(sourceDidWork()); } else { - files.visit(new FileVisitor() { + cleanFiles.visit(new FileVisitor() { @Override public void visitDir(FileVisitDetails fileVisitDetails) { @@ -51,6 +55,10 @@ public void visitFile(FileVisitDetails fileVisitDetails) { } } }); + if (!lintsFiles.isEmpty()) { + boolean detailed = false; + throw new GradleException(super.allLintsErrorMsgDetailed(lintsFiles, detailed)); + } } } } 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 55336a00ad..175a828a66 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-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,17 +29,24 @@ import org.gradle.api.file.FileVisitDetails; import org.gradle.api.file.FileVisitor; import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +import org.jetbrains.annotations.NotNull; import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; +@DisableCachingByDefault(because = "not worth caching") public abstract class SpotlessCheck extends SpotlessTaskService.ClientTask { @Internal public abstract Property getEncoding(); + @Input + public abstract Property getRunToFixMessage(); + public void performActionTest() throws IOException { performAction(true); } @@ -50,65 +57,78 @@ public void performAction() throws IOException { } private void performAction(boolean isTest) throws IOException { - ConfigurableFileTree files = getConfigCacheWorkaround().fileTree().from(getSpotlessOutDirectory().get()); - if (files.isEmpty()) { + ConfigurableFileTree cleanFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessCleanDirectory().get()); + ConfigurableFileTree lintsFiles = getConfigCacheWorkaround().fileTree().from(getSpotlessLintsDirectory().get()); + if (cleanFiles.isEmpty() && lintsFiles.isEmpty()) { getState().setDidWork(sourceDidWork()); } else if (!isTest && applyHasRun()) { // if our matching apply has already run, then we don't need to do anything getState().setDidWork(false); } else { - List problemFiles = new ArrayList<>(); - files.visit(new FileVisitor() { - @Override - public void visitDir(FileVisitDetails fileVisitDetails) { - - } - - @Override - public void visitFile(FileVisitDetails fileVisitDetails) { - String path = fileVisitDetails.getPath(); - File originalSource = new File(getProjectDir().get().getAsFile(), path); - try { - // read the file on disk - byte[] userFile = Files.readAllBytes(originalSource.toPath()); - // and the formatted version from spotlessOutDirectory - byte[] formatted; - { - ByteArrayOutputStream clean = new ByteArrayOutputStream(); - fileVisitDetails.copyTo(clean); - formatted = clean.toByteArray(); - } - // If these two are equal, it means that SpotlessTask left a file - // in its output directory which ought to have been removed. As - // best I can tell, this is a filesytem race which is very hard - // to trigger. GitRatchetGradleTest can *sometimes* reproduce it - // but it's very erratic, and that test writes both to gradle cache - // and git cache very quickly. Either of gradle or jgit might be - // caching something wrong because of the fast repeated writes. - if (!Arrays.equals(userFile, formatted)) { - // If the on-disk content is equal to the formatted content, - // just don't add it as a problem file. Easy! - problemFiles.add(originalSource); - } - } catch (IOException e) { - throw ThrowingEx.asRuntime(e); - } - } - }); - if (!problemFiles.isEmpty()) { - Collections.sort(problemFiles); + List unformattedFiles = getUncleanFiles(cleanFiles); + if (!unformattedFiles.isEmpty()) { + // if any files are unformatted, we show those throw new GradleException(DiffMessageFormatter.builder() - .runToFix("Run '" + calculateGradleCommand() + " " + getTaskPathPrefix() + "spotlessApply' to fix these violations.") + .runToFix(getRunToFixMessage().get()) .formatterFolder( getProjectDir().get().getAsFile().toPath(), - getSpotlessOutDirectory().get().toPath(), + getSpotlessCleanDirectory().get().toPath(), getEncoding().get()) - .problemFiles(problemFiles) + .problemFiles(unformattedFiles) .getMessage()); + } else { + // We only show lints if there are no unformatted files. + // This is because lint line numbers are relative to the + // formatted content, and formatting often fixes lints. + boolean detailed = false; + throw new GradleException(super.allLintsErrorMsgDetailed(lintsFiles, detailed)); } } } + private @NotNull List getUncleanFiles(ConfigurableFileTree cleanFiles) { + List uncleanFiles = new ArrayList<>(); + cleanFiles.visit(new FileVisitor() { + @Override + public void visitDir(FileVisitDetails fileVisitDetails) { + + } + + @Override + public void visitFile(FileVisitDetails fileVisitDetails) { + String path = fileVisitDetails.getPath(); + File originalSource = new File(getProjectDir().get().getAsFile(), path); + try { + // read the file on disk + byte[] userFile = Files.readAllBytes(originalSource.toPath()); + // and the formatted version from spotlessOutDirectory + byte[] formatted; + { + ByteArrayOutputStream clean = new ByteArrayOutputStream(); + fileVisitDetails.copyTo(clean); + formatted = clean.toByteArray(); + } + // If these two are equal, it means that SpotlessTask left a file + // in its output directory which ought to have been removed. As + // best I can tell, this is a filesytem race which is very hard + // to trigger. GitRatchetGradleTest can *sometimes* reproduce it + // but it's very erratic, and that test writes both to Gradle cache + // and git cache very quickly. Either of Gradle or jgit might be + // caching something wrong because of the fast repeated writes. + if (!Arrays.equals(userFile, formatted)) { + // If the on-disk content is equal to the formatted content, + // just don't add it as a problem file. Easy! + uncleanFiles.add(originalSource); + } + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + }); + Collections.sort(uncleanFiles); + return uncleanFiles; + } + @Internal abstract Property getProjectPath(); @@ -117,6 +137,8 @@ void init(SpotlessTaskImpl impl) { super.init(impl); getProjectPath().set(getProject().getPath()); getEncoding().set(impl.getEncoding()); + getRunToFixMessage().convention( + "Run '" + calculateGradleCommand() + " " + getTaskPathPrefix() + "spotlessApply' to fix these violations."); } private String getTaskPathPrefix() { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessDiagnoseTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessDiagnoseTask.java index f20957e650..68f6e63e75 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessDiagnoseTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessDiagnoseTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,14 @@ import org.gradle.api.DefaultTask; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.UntrackedTask; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.PaddedCell; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +@UntrackedTask(because = "undeclared inputs/outputs") public class SpotlessDiagnoseTask extends DefaultTask { SpotlessTask source; @@ -43,7 +45,8 @@ public SpotlessTask getSource() { @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public void performAction() throws IOException { Path srcRoot = getProject().getProjectDir().toPath(); - Path diagnoseRoot = getProject().getBuildDir().toPath().resolve("spotless-diagnose-" + source.formatName()); + Path diagnoseRoot = getProject().getLayout().getBuildDirectory().getAsFile().get() + .toPath().resolve("spotless-diagnose-" + source.formatName()); getProject().delete(diagnoseRoot.toFile()); try (Formatter formatter = source.buildFormatter()) { for (File file : source.target) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 2fd3033f5f..e883953eaa 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.gradle.api.Project; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; import com.diffplug.spotless.LineEnding; @@ -36,7 +37,7 @@ public abstract class SpotlessExtension { final Project project; private final RegisterDependenciesTask registerDependenciesTask; - protected static final String TASK_GROUP = "Verification"; + protected static final String TASK_GROUP = LifecycleBasePlugin.VERIFICATION_GROUP; protected static final String CHECK_DESCRIPTION = "Checks that sourcecode satisfies formatting steps."; protected static final String APPLY_DESCRIPTION = "Applies code formatting steps to sourcecode in-place."; @@ -56,13 +57,16 @@ RegisterDependenciesTask getRegisterDependenciesTask() { } /** Line endings (if any). */ - LineEnding lineEndings = LineEnding.GIT_ATTRIBUTES; + LineEnding lineEndings = LineEnding.GIT_ATTRIBUTES_FAST_ALLSAME; public LineEnding getLineEndings() { return lineEndings; } public void setLineEndings(LineEnding lineEndings) { + if (lineEndings == LineEnding.GIT_ATTRIBUTES) { + throw new IllegalArgumentException("GIT_ATTRIBUTES not supported in Gradle, use GIT_ATTRIBUTES_FAST_ALLSAME instead. See https://github.com/diffplug/spotless/issues/1274 for more details."); + } this.lineEndings = requireNonNull(lineEndings); } @@ -73,6 +77,11 @@ public Charset getEncoding() { return encoding; } + /** Sets encoding to use (defaults to UTF_8). */ + public void setEncoding(Charset charset) { + encoding = requireNonNull(charset); + } + /** Sets encoding to use (defaults to UTF_8). */ public void setEncoding(String name) { requireNonNull(name); @@ -80,8 +89,8 @@ public void setEncoding(String name) { } /** Sets encoding to use (defaults to UTF_8). */ - public void setEncoding(Charset charset) { - encoding = requireNonNull(charset); + public void encoding(Charset charset) { + setEncoding(charset); } /** Sets encoding to use (defaults to UTF_8). */ @@ -141,6 +150,12 @@ public void freshmark(Action closure) { format(FreshMarkExtension.NAME, FreshMarkExtension.class, closure); } + /** Configures the special flexmark-specific extension. */ + public void flexmark(Action closure) { + requireNonNull(closure); + format(FlexmarkExtension.NAME, FlexmarkExtension.class, closure); + } + /** Configures the special groovy-specific extension. */ public void groovy(Action closure) { format(GroovyExtension.NAME, GroovyExtension.class, closure); @@ -161,6 +176,11 @@ public void cpp(Action closure) { format(CppExtension.NAME, CppExtension.class, closure); } + /** Configures the special javascript-specific extension for javascript files. */ + public void javascript(Action closure) { + format(JavascriptExtension.NAME, JavascriptExtension.class, closure); + } + /** Configures the special typescript-specific extension for typescript files. */ public void typescript(Action closure) { format(TypescriptExtension.NAME, TypescriptExtension.class, closure); @@ -182,6 +202,47 @@ public void json(Action closure) { format(JsonExtension.NAME, JsonExtension.class, closure); } + /** Configures the special protobuf-specific extension. */ + public void protobuf(Action closure) { + requireNonNull(closure); + format(ProtobufExtension.NAME, ProtobufExtension.class, closure); + } + + /** Configures the special shell-specific extension. */ + public void shell(Action closure) { + requireNonNull(closure); + format(ShellExtension.NAME, ShellExtension.class, closure); + } + + /** Configures the special YAML-specific extension. */ + public void yaml(Action closure) { + requireNonNull(closure); + format(YamlExtension.NAME, YamlExtension.class, closure); + } + + /** Configures the special Gherkin-specific extension. */ + public void gherkin(Action closure) { + requireNonNull(closure); + format(GherkinExtension.NAME, GherkinExtension.class, closure); + } + + public void go(Action closure) { + requireNonNull(closure); + format(GoExtension.NAME, GoExtension.class, closure); + } + + /** Configures the special CSS-specific extension. */ + public void css(Action closure) { + requireNonNull(closure); + format(CssExtension.NAME, CssExtension.class, closure); + } + + /** Configures the special POM-specific extension. */ + public void pom(Action closure) { + requireNonNull(closure); + format(PomExtension.NAME, PomExtension.class, closure); + } + /** Configures a custom extension. */ public void format(String name, Action closure) { requireNonNull(name, "name"); @@ -199,7 +260,7 @@ public boolean isEnforceCheck() { /** * Configures Gradle's {@code check} task to run {@code spotlessCheck} if {@code true}, * but to not do so if {@code false}. - * + *

* {@code true} by default. */ public void setEnforceCheck(boolean enforceCheck) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index 24fa7b54b0..75168f690a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import org.gradle.api.Action; import org.gradle.api.Project; -import org.gradle.api.UnknownTaskException; +import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.tasks.TaskContainer; import org.gradle.api.tasks.TaskProvider; @@ -41,28 +41,25 @@ public SpotlessExtensionImpl(Project project) { project.afterEvaluate(unused -> { if (enforceCheck) { - try { - project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME) - .configure(task -> task.dependsOn(rootCheckTask)); - } catch (UnknownTaskException e) { - // no action needed, it's okay if there's no `check` task - } + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME).configure(task -> task.dependsOn(rootCheckTask)); } }); } @Override protected void createFormatTasks(String name, FormatExtension formatExtension) { - boolean isIdeHook = project.hasProperty(IdeHook.PROPERTY); + IdeHook.State ideHook = new IdeHook.State(project); TaskContainer tasks = project.getTasks(); // create the SpotlessTask String taskName = EXTENSION + SpotlessPlugin.capitalize(name); TaskProvider spotlessTask = tasks.register(taskName, SpotlessTaskImpl.class, task -> { task.init(getRegisterDependenciesTask().getTaskService()); - task.setEnabled(!isIdeHook); + task.setGroup(TASK_GROUP); + task.getIdeHookState().set(ideHook); + // clean removes the SpotlessCache, so we have to run after clean + task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME); }); - SpotlessPlugin.taskMustRunAfterClean(project, spotlessTask); project.afterEvaluate(unused -> { spotlessTask.configure(task -> { // now that the task is being configured, we execute our actions @@ -77,22 +74,19 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { // create the check and apply control tasks TaskProvider applyTask = tasks.register(taskName + APPLY, SpotlessApply.class, task -> { task.init(spotlessTask.get()); - task.setEnabled(!isIdeHook); + task.setGroup(TASK_GROUP); + task.setEnabled(ideHook.path == null); task.dependsOn(spotlessTask); }); rootApplyTask.configure(task -> { - task.dependsOn(applyTask); - - if (isIdeHook) { - // the rootApplyTask is no longer just a marker task, now it does a bit of work itself - task.doLast(unused -> IdeHook.performHook(spotlessTask.get())); - } + task.dependsOn(ideHook.path == null ? applyTask : spotlessTask); }); TaskProvider checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> { SpotlessTaskImpl source = spotlessTask.get(); + task.setGroup(TASK_GROUP); task.init(source); - task.setEnabled(!isIdeHook); + task.setEnabled(ideHook.path == null); task.dependsOn(source); // if the user runs both, make sure that apply happens first, @@ -103,8 +97,9 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { // create the diagnose task TaskProvider diagnoseTask = tasks.register(taskName + DIAGNOSE, SpotlessDiagnoseTask.class, task -> { task.source = spotlessTask.get(); + task.setGroup(TASK_GROUP); + task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME); }); - SpotlessPlugin.taskMustRunAfterClean(project, diagnoseTask); rootDiagnoseTask.configure(task -> task.dependsOn(diagnoseTask)); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java index a086f1ca52..ae6243f53a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import org.gradle.api.Action; import org.gradle.api.Project; +import com.diffplug.spotless.LazyForwardingEquality; + public class SpotlessExtensionPredeclare extends SpotlessExtension { private final SortedMap toSetup = new TreeMap<>(); @@ -33,6 +35,8 @@ public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy pol lazyAction.execute(formatExtension); } getRegisterDependenciesTask().steps.addAll(formatExtension.steps); + // needed to fix Deemon memory leaks (#1194), but this line came from https://github.com/diffplug/spotless/pull/1206 + LazyForwardingEquality.unlazy(getRegisterDependenciesTask().steps); }); }); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java index 7b9e61eb03..24bea9d90b 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,30 +15,39 @@ */ package com.diffplug.gradle.spotless; -import java.util.function.Consumer; - import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.plugins.BasePlugin; -import org.gradle.api.tasks.Delete; -import org.gradle.api.tasks.TaskProvider; +import org.gradle.util.GradleVersion; +import com.diffplug.spotless.Jvm; import com.diffplug.spotless.SpotlessCache; public class SpotlessPlugin implements Plugin { static final String SPOTLESS_MODERN = "spotlessModern"; - static final String MINIMUM_GRADLE = "6.1.1"; + static final String VER_GRADLE_min = "6.1.1"; + static final String VER_GRADLE_javaPluginExtension = "7.1"; + static final String VER_GRADLE_minVersionForCustom = "8.4"; + private static final int MINIMUM_JRE = 11; @Override public void apply(Project project) { - if (SpotlessPluginRedirect.gradleIsTooOld(project)) { - throw new GradleException("Spotless requires Gradle " + MINIMUM_GRADLE + " or newer, this was " + project.getGradle().getGradleVersion()); + if (SpotlessPluginRedirect.gradleIsTooOld()) { + throw new GradleException("Spotless requires Gradle " + VER_GRADLE_min + " or newer, this was " + GradleVersion.current().getVersion()); + } + if (Jvm.version() < MINIMUM_JRE) { + throw new GradleException("Spotless requires JRE " + MINIMUM_JRE + " or newer, this was " + JavaVersion.current() + ".\n" + + "You can upgrade your build JRE and still compile for older targets, see below\n" + + "https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation"); } // if -PspotlessModern=true, then use the modern stuff instead of the legacy stuff if (project.hasProperty(SPOTLESS_MODERN)) { project.getLogger().warn("'spotlessModern' has no effect as of Spotless 5.0, recommend removing it."); } + // make sure there's a `clean` and a `check` + project.getPlugins().apply(BasePlugin.class); // setup the extension project.getExtensions().create(SpotlessExtension.class, SpotlessExtension.EXTENSION, SpotlessExtensionImpl.class, project); @@ -50,26 +59,7 @@ public void apply(Project project) { // // we use System.identityHashCode() to avoid a memory leak by hanging on to the reference directly int cacheKey = System.identityHashCode(project.getRootProject()); - configureCleanTask(project, clean -> clean.doLast(unused -> SpotlessCache.clearOnce(cacheKey))); - } - - static void configureCleanTask(Project project, Consumer onClean) { - project.getTasks().withType(Delete.class).configureEach(clean -> { - if (clean.getName().equals(BasePlugin.CLEAN_TASK_NAME)) { - onClean.accept(clean); - } - }); - } - - /** clean removes the SpotlessCache, so we have to run after clean. */ - static void taskMustRunAfterClean(Project project, TaskProvider task) { - if (project.getPlugins().hasPlugin(BasePlugin.class)) { - // if we know that the clean task is around, then we can configure lazily - task.configure(t -> t.mustRunAfter(BasePlugin.CLEAN_TASK_NAME)); - } else { - // otherwise, we trigger configuration when the clean task gets configured - configureCleanTask(project, clean -> task.get().mustRunAfter(clean)); - } + project.getTasks().named(BasePlugin.CLEAN_TASK_NAME).configure(clean -> clean.doLast(unused -> SpotlessCache.clearOnce(cacheKey))); } static String capitalize(String input) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPluginRedirect.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPluginRedirect.java index da4d3dcd8b..801390be2f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPluginRedirect.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPluginRedirect.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,14 @@ import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.util.GradleVersion; import com.diffplug.common.base.StringPrinter; public class SpotlessPluginRedirect implements Plugin { private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)"); - private static int badSemver(String input) { + static int badSemver(String input) { Matcher matcher = BAD_SEMVER.matcher(input); if (!matcher.find() || matcher.start() != 0) { throw new IllegalArgumentException("Version must start with " + BAD_SEMVER.pattern()); @@ -37,18 +38,17 @@ private static int badSemver(String input) { return badSemver(Integer.parseInt(major), Integer.parseInt(minor)); } + static int badSemverOfGradle() { + return badSemver(GradleVersion.current().getVersion()); + } + /** Ambiguous after 2147.483647.blah-blah */ private static int badSemver(int major, int minor) { return major * 1_000_000 + minor; } - static Boolean gradleIsTooOld; - - static boolean gradleIsTooOld(Project project) { - if (gradleIsTooOld == null) { - gradleIsTooOld = badSemver(project.getGradle().getGradleVersion()) < badSemver(SpotlessPlugin.MINIMUM_GRADLE); - } - return gradleIsTooOld.booleanValue(); + static boolean gradleIsTooOld() { + return badSemverOfGradle() < badSemver(SpotlessPlugin.VER_GRADLE_min); } @Override @@ -72,8 +72,8 @@ public void apply(Project project) { "", "If you like the idea behind 'ratchetFrom', you should checkout spotless-changelog", "https://github.com/diffplug/spotless-changelog"); - if (gradleIsTooOld(project)) { - errorMsg = errorMsg.replace("To migrate:\n", "To migrate:\n- Upgrade gradle to " + SpotlessPlugin.MINIMUM_GRADLE + " or newer (you're on " + project.getGradle().getGradleVersion() + ")\n"); + if (gradleIsTooOld()) { + errorMsg = errorMsg.replace("To migrate:\n", "To migrate:\n- Upgrade Gradle to " + SpotlessPlugin.VER_GRADLE_min + " or newer (you're on " + GradleVersion.current().getVersion() + ")\n"); } throw new GradleException(errorMsg); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java index 6f2279b2e0..713c9a2e86 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2025 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.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -28,30 +27,28 @@ import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; +import org.gradle.work.DisableCachingByDefault; import org.gradle.work.Incremental; -import com.diffplug.gradle.spotless.JvmLocalCache.LiveCache; -import com.diffplug.spotless.FormatExceptionPolicy; -import com.diffplug.spotless.FormatExceptionPolicyStrict; +import com.diffplug.spotless.ConfigurationCacheHackList; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.extra.GitRatchet; +@DisableCachingByDefault(because = "abstract definition") public abstract class SpotlessTask extends DefaultTask { @Internal abstract Property getTaskService(); - protected LiveCache createLive(String keyName) { - return JvmLocalCache.createLive(this, keyName); - } - // set by SpotlessExtension, but possibly overridden by FormatExtension protected String encoding = "UTF-8"; @@ -64,26 +61,34 @@ public void setEncoding(String encoding) { this.encoding = Objects.requireNonNull(encoding); } - protected final LiveCache lineEndingsPolicy = createLive("lineEndingsPolicy"); + protected Provider lineEndingsPolicy = null; @Input - public LineEnding.Policy getLineEndingsPolicy() { - return lineEndingsPolicy.get(); + public Provider getLineEndingsPolicy() { + return lineEndingsPolicy; } - public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) { - this.lineEndingsPolicy.set(lineEndingsPolicy); + public void setLineEndingsPolicy(Provider lineEndingsPolicy) { + this.lineEndingsPolicy = lineEndingsPolicy; } - /** The sha of the tree at repository root, used for determining if an individual *file* is clean according to git. */ + /** + * The sha of the tree at repository root, used for determining if an individual + * *file* is clean according to git. + */ private transient ObjectId rootTreeSha; /** - * The sha of the tree at the root of *this project*, used to determine if the git baseline has changed within this folder. - * Using a more fine-grained tree (rather than the project root) allows Gradle to mark more subprojects as up-to-date + * The sha of the tree at the root of *this project*, used to determine if the + * git baseline has changed within this folder. + * Using a more fine-grained tree (rather than the project root) allows Gradle + * to mark more subprojects as up-to-date * compared to using the project root. */ private transient ObjectId subtreeSha = ObjectId.zeroId(); - /** Stored so that the configuration cache can recreate the GitRatchetGradle state. */ + /** + * Stored so that the configuration cache can recreate the GitRatchetGradle + * state. + */ protected String ratchetFrom; public void setupRatchet(String ratchetFrom) { @@ -119,15 +124,15 @@ public ObjectId getRatchetSha() { return subtreeSha; } - protected FormatExceptionPolicy exceptionPolicy = new FormatExceptionPolicyStrict(); + protected List lintSuppressions = new ArrayList<>(); - public void setExceptionPolicy(FormatExceptionPolicy exceptionPolicy) { - this.exceptionPolicy = Objects.requireNonNull(exceptionPolicy); + public void setLintSuppressions(List lintSuppressions) { + this.lintSuppressions = Objects.requireNonNull(lintSuppressions); } @Input - public FormatExceptionPolicy getExceptionPolicy() { - return exceptionPolicy; + public List getLintSuppressions() { + return lintSuppressions; } protected FileCollection target; @@ -147,29 +152,41 @@ public void setTarget(Iterable target) { } } - protected File outputDirectory = new File(getProject().getBuildDir(), "spotless/" + getName()); + protected File cleanDirectory = new File(getProject().getLayout().getBuildDirectory().getAsFile().get(), + "spotless-clean/" + getName()); + + @OutputDirectory + public File getCleanDirectory() { + return cleanDirectory; + } + + protected File lintsDirectory = new File(getProject().getLayout().getBuildDirectory().getAsFile().get(), + "spotless-lints/" + getName()); @OutputDirectory - public File getOutputDirectory() { - return outputDirectory; + public File getLintsDirectory() { + return lintsDirectory; } - protected final LiveCache> steps = createLive("steps"); - { - steps.set(new ArrayList()); + private final ConfigurationCacheHackList stepsInternalRoundtrip = ConfigurationCacheHackList.forRoundtrip(); + private final ConfigurationCacheHackList stepsInternalEquality = ConfigurationCacheHackList.forEquality(); + + @Internal + public ConfigurationCacheHackList getStepsInternalRoundtrip() { + return stepsInternalRoundtrip; } @Input - public List getSteps() { - return Collections.unmodifiableList(steps.get()); + public ConfigurationCacheHackList getStepsInternalEquality() { + return stepsInternalEquality; } public void setSteps(List steps) { - this.steps.set(PluginGradlePreconditions.requireElementsNonNull(steps)); - } - - public boolean addStep(FormatterStep step) { - return this.steps.get().add(Objects.requireNonNull(step)); + PluginGradlePreconditions.requireElementsNonNull(steps); + this.stepsInternalRoundtrip.clear(); + this.stepsInternalEquality.clear(); + this.stepsInternalRoundtrip.addAll(steps); + this.stepsInternalEquality.addAll(steps); } /** Returns the name of this format. */ @@ -184,11 +201,9 @@ String formatName() { Formatter buildFormatter() { return Formatter.builder() - .lineEndingsPolicy(lineEndingsPolicy.get()) + .lineEndingsPolicy(getLineEndingsPolicy().get()) .encoding(Charset.forName(encoding)) - .rootDir(getProjectDir().get().getAsFile().toPath()) - .steps(steps.get()) - .exceptionPolicy(exceptionPolicy) + .steps(stepsInternalRoundtrip.getSteps()) .build(); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java index 1b9cda204c..7241f018d0 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.LinkedHashMap; +import java.util.List; import javax.annotation.Nullable; import javax.inject.Inject; @@ -27,34 +29,59 @@ import org.gradle.api.GradleException; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.work.ChangeType; import org.gradle.work.FileChange; import org.gradle.work.InputChanges; +import com.diffplug.common.annotations.VisibleForTesting; import com.diffplug.common.base.StringPrinter; import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.PaddedCell; +import com.diffplug.spotless.Lint; +import com.diffplug.spotless.LintState; import com.diffplug.spotless.extra.GitRatchet; @CacheableTask public abstract class SpotlessTaskImpl extends SpotlessTask { + @Input + @Optional + abstract Property getIdeHookState(); + @Internal abstract DirectoryProperty getProjectDir(); void init(Provider service) { + taskServiceProvider = service; + SpotlessTaskService.usesServiceTolerateTestFailure(this, service); getTaskService().set(service); getProjectDir().set(getProject().getProjectDir()); } + // this field is stupid, but we need it, see https://github.com/diffplug/spotless/issues/1260 + private transient Provider taskServiceProvider; + + @Internal + Provider getTaskServiceProvider() { + return taskServiceProvider; + } + @Inject protected abstract FileSystemOperations getFs(); @TaskAction public void performAction(InputChanges inputs) throws Exception { + IdeHook.State ideHook = getIdeHookState().getOrNull(); + if (ideHook != null && ideHook.path != null) { + IdeHook.performHook(this, ideHook); + return; + } + SpotlessTaskService taskService = getTaskService().get(); taskService.registerSourceAlreadyRan(this); if (target == null) { @@ -63,70 +90,83 @@ public void performAction(InputChanges inputs) throws Exception { if (!inputs.isIncremental()) { getLogger().info("Not incremental: removing prior outputs"); - getFs().delete(d -> d.delete(outputDirectory)); - Files.createDirectories(outputDirectory.toPath()); + getFs().delete(d -> d.delete(cleanDirectory)); + getFs().delete(d -> d.delete(lintsDirectory)); + Files.createDirectories(cleanDirectory.toPath()); + Files.createDirectories(lintsDirectory.toPath()); } try (Formatter formatter = buildFormatter()) { GitRatchetGradle ratchet = getRatchet(); for (FileChange fileChange : inputs.getFileChanges(target)) { File input = fileChange.getFile(); + File projectDir = getProjectDir().get().getAsFile(); + String relativePath = FormatExtension.relativize(projectDir, input); + if (relativePath == null) { + throw new IllegalArgumentException(StringPrinter.buildString(printer -> { + printer.println("Spotless error! All target files must be within the project dir."); + printer.println(" project dir: " + projectDir.getAbsolutePath()); + printer.println(" target: " + input.getAbsolutePath()); + })); + } if (fileChange.getChangeType() == ChangeType.REMOVED) { - deletePreviousResult(input); + deletePreviousResults(cleanDirectory, relativePath); + deletePreviousResults(lintsDirectory, relativePath); } else { if (input.isFile()) { - processInputFile(ratchet, formatter, input); + processInputFile(ratchet, formatter, input, relativePath); } } } } } - private void processInputFile(@Nullable GitRatchet ratchet, Formatter formatter, File input) throws IOException { - File output = getOutputFile(input); - getLogger().debug("Applying format to " + input + " and writing to " + output); - PaddedCell.DirtyState dirtyState; + @VisibleForTesting + void processInputFile(@Nullable GitRatchet ratchet, Formatter formatter, File input, String relativePath) throws IOException { + File cleanFile = new File(cleanDirectory, relativePath); + File lintFile = new File(lintsDirectory, relativePath); + getLogger().debug("Applying format to {} and writing to {}", input, cleanFile); + LintState lintState; if (ratchet != null && ratchet.isClean(getProjectDir().get().getAsFile(), getRootTreeSha(), input)) { - dirtyState = PaddedCell.isClean(); + lintState = LintState.clean(); } else { - dirtyState = PaddedCell.calculateDirtyState(formatter, input); + try { + lintState = LintState.of(formatter, input).withRemovedSuppressions(formatter, relativePath, getLintSuppressions()); + } catch (Throwable e) { + throw new IllegalArgumentException("Issue processing file: " + input, e); + } } - if (dirtyState.isClean()) { + if (lintState.getDirtyState().isClean()) { // Remove previous output if it exists - Files.deleteIfExists(output.toPath()); - } else if (dirtyState.didNotConverge()) { - getLogger().warn("Skipping '" + input + "' because it does not converge. Run {@code spotlessDiagnose} to understand why"); + Files.deleteIfExists(cleanFile.toPath()); + } else if (lintState.getDirtyState().didNotConverge()) { + getLogger().warn("Skipping '{}' because it does not converge. Run {@code spotlessDiagnose} to understand why", relativePath); } else { - Path parentDir = output.toPath().getParent(); + Path parentDir = cleanFile.toPath().getParent(); if (parentDir == null) { - throw new IllegalStateException("Every file has a parent folder."); + throw new IllegalStateException("Every file has a parent folder. But not: " + cleanFile); } Files.createDirectories(parentDir); // Need to copy the original file to the tmp location just to remember the file attributes - Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); - dirtyState.writeCanonicalTo(output); + Files.copy(input.toPath(), cleanFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + + getLogger().info(String.format("Writing clean file: %s", cleanFile)); + lintState.getDirtyState().writeCanonicalTo(cleanFile); + } + if (!lintState.isHasLints()) { + Files.deleteIfExists(lintFile.toPath()); + } else { + LinkedHashMap> lints = lintState.getLintsByStep(formatter); + SerializableMisc.toFile(lints, lintFile); } } - private void deletePreviousResult(File input) throws IOException { - File output = getOutputFile(input); + private void deletePreviousResults(File baseDir, String subpath) throws IOException { + File output = new File(baseDir, subpath); if (output.isDirectory()) { getFs().delete(d -> d.delete(output)); } else { Files.deleteIfExists(output.toPath()); } } - - private File getOutputFile(File input) { - File projectDir = getProjectDir().get().getAsFile(); - String outputFileName = FormatExtension.relativize(projectDir, input); - if (outputFileName == null) { - throw new IllegalArgumentException(StringPrinter.buildString(printer -> { - printer.println("Spotless error! All target files must be within the project dir."); - printer.println(" project dir: " + projectDir.getAbsolutePath()); - printer.println(" target: " + input.getAbsolutePath()); - })); - } - return new File(outputDirectory, outputFileName); - } } 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 7fc0ea3775..b58ff9005a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,23 @@ import java.io.File; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import javax.inject.Inject; import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileVisitDetails; +import org.gradle.api.file.FileVisitor; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.services.BuildService; import org.gradle.api.services.BuildServiceParameters; import org.gradle.api.tasks.Internal; @@ -35,6 +43,7 @@ import com.diffplug.common.base.Preconditions; import com.diffplug.common.base.Unhandled; +import com.diffplug.spotless.Lint; import com.diffplug.spotless.Provisioner; /** @@ -91,9 +100,22 @@ public void close() throws Exception { static String INDEPENDENT_HELPER = "Helper"; + static void usesServiceTolerateTestFailure(DefaultTask task, Provider serviceProvider) { + try { + task.usesService(serviceProvider); + } catch (ClassCastException e) { + // this happens only in our test mocking, e.g. DiffMessageFormatterTest + // https://github.com/diffplug/spotless/pull/1570/commits/c45e1f2322c78f272689feb35753bbc633422bfa + // it's fine to swallow these exceptions + } + } + static abstract class ClientTask extends DefaultTask { @Internal - abstract Property getSpotlessOutDirectory(); + abstract Property getSpotlessCleanDirectory(); + + @Internal + abstract Property getSpotlessLintsDirectory(); @Internal abstract Property getTaskService(); @@ -105,7 +127,9 @@ static abstract class ClientTask extends DefaultTask { protected abstract ObjectFactory getConfigCacheWorkaround(); void init(SpotlessTaskImpl impl) { - getSpotlessOutDirectory().set(impl.getOutputDirectory()); + usesServiceTolerateTestFailure(this, impl.getTaskServiceProvider()); + getSpotlessCleanDirectory().set(impl.getCleanDirectory()); + getSpotlessLintsDirectory().set(impl.getLintsDirectory()); getTaskService().set(impl.getTaskService()); getProjectDir().set(impl.getProjectDir()); } @@ -142,5 +166,41 @@ protected boolean sourceDidWork() { protected boolean applyHasRun() { return service().apply.containsKey(sourceTaskPath()); } + + protected String allLintsErrorMsgDetailed(ConfigurableFileTree lintsFiles, boolean detailed) { + AtomicInteger total = new AtomicInteger(0); + TreeMap>> allLints = new TreeMap<>(); + lintsFiles.visit(new FileVisitor() { + @Override + public void visitDir(FileVisitDetails fileVisitDetails) { + + } + + @Override + public void visitFile(FileVisitDetails fileVisitDetails) { + String path = fileVisitDetails.getPath(); + getLogger().debug("Reading lints for " + path); + LinkedHashMap> lints = SerializableMisc.fromFile(LinkedHashMap.class, fileVisitDetails.getFile()); + allLints.put(path, lints); + lints.values().forEach(list -> total.addAndGet(list.size())); + } + }); + StringBuilder builder = new StringBuilder(); + builder.append("There were " + total.get() + " lint error(s), they must be fixed or suppressed.\n"); + for (Map.Entry>> lintsPerFile : allLints.entrySet()) { + for (Map.Entry> stepLints : lintsPerFile.getValue().entrySet()) { + String stepName = stepLints.getKey(); + for (Lint lint : stepLints.getValue()) { + builder.append(lintsPerFile.getKey()); + builder.append(":"); + boolean oneLine = !detailed; + lint.addWarningMessageTo(builder, stepName, oneLine); + builder.append("\n"); + } + } + } + builder.append("Resolve these lints or suppress with `suppressLintsFor`"); + return builder.toString(); + } } } 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 58b9d4acbb..39e050d51e 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-2020 DiffPlug + * Copyright 2016-2024 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 java.util.Objects.requireNonNull; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -27,7 +28,12 @@ import org.gradle.api.Project; +import com.diffplug.gradle.spotless.JavascriptExtension.EslintBaseConfig; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.npm.EslintConfig; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintTypescriptConfig; import com.diffplug.spotless.npm.NpmPathResolver; import com.diffplug.spotless.npm.PrettierFormatterStep; import com.diffplug.spotless.npm.TsConfigFileType; @@ -53,7 +59,10 @@ public TypescriptFormatExtension tsfmt(String version) { return tsfmt(TsFmtFormatterStep.defaultDevDependenciesWithTsFmt(version)); } - /** Creates a {@code TypescriptFormatExtension} using exactly the specified npm packages. */ + /** + * Creates a {@code TypescriptFormatExtension} using exactly the specified npm + * packages. + */ public TypescriptFormatExtension tsfmt(Map devDependencies) { TypescriptFormatExtension tsfmt = new TypescriptFormatExtension(devDependencies); addStep(tsfmt.createStep()); @@ -73,46 +82,48 @@ public class TypescriptFormatExtension extends NpmStepConfig devDependencies; TypescriptFormatExtension(Map devDependencies) { + super(getProject(), TypescriptExtension.this::replaceStep); this.devDependencies = Objects.requireNonNull(devDependencies); } - public void config(final Map config) { + public TypescriptFormatExtension config(final Map config) { this.config = new TreeMap<>(requireNonNull(config)); - replaceStep(createStep()); + replaceStep(); + return this; } - public void tsconfigFile(final Object path) { - configFile(TsConfigFileType.TSCONFIG, path); + public TypescriptFormatExtension tsconfigFile(final Object path) { + return configFile(TsConfigFileType.TSCONFIG, path); } - public void tslintFile(final Object path) { - configFile(TsConfigFileType.TSLINT, path); + public TypescriptFormatExtension tslintFile(final Object path) { + return configFile(TsConfigFileType.TSLINT, path); } - public void vscodeFile(final Object path) { - configFile(TsConfigFileType.VSCODE, path); + public TypescriptFormatExtension vscodeFile(final Object path) { + return configFile(TsConfigFileType.VSCODE, path); } - public void tsfmtFile(final Object path) { - configFile(TsConfigFileType.TSFMT, path); + public TypescriptFormatExtension tsfmtFile(final Object path) { + return configFile(TsConfigFileType.TSFMT, path); } - private void configFile(TsConfigFileType filetype, Object path) { + private TypescriptFormatExtension configFile(TsConfigFileType filetype, Object path) { this.configFileType = requireNonNull(filetype); this.configFilePath = requireNonNull(path); - replaceStep(createStep()); + replaceStep(); + return this; } public FormatterStep createStep() { final Project project = getProject(); - return TsFmtFormatterStep.create( - devDependencies, - provisioner(), - project.getBuildDir(), - new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()), - typedConfigFile(), - config); + return TsFmtFormatterStep + .create(devDependencies, provisioner(), project.getProjectDir(), + project.getLayout().getBuildDirectory().getAsFile().get(), npmModulesCacheOrNull(), + new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), + Arrays.asList(project.getProjectDir(), project.getRootDir())), + typedConfigFile(), config); } private TypedTsFmtConfigFile typedConfigFile() { @@ -144,7 +155,8 @@ public PrettierConfig prettier(Map devDependencies) { } /** - * Overrides the parser to be set to typescript, no matter what the user's config says. + * Overrides the parser to be set to typescript, no matter what the user's + * config says. */ public class TypescriptPrettierConfig extends PrettierConfig { TypescriptPrettierConfig(Map devDependencies) { @@ -152,26 +164,112 @@ public class TypescriptPrettierConfig extends PrettierConfig { } @Override - FormatterStep createStep() { + protected FormatterStep createStep() { fixParserToTypescript(); return super.createStep(); } private void fixParserToTypescript() { if (this.prettierConfig == null) { - this.prettierConfig = Collections.singletonMap("parser", "typescript"); + this.prettierConfig = new TreeMap<>(Collections.singletonMap("parser", "typescript")); } else { final Object replaced = this.prettierConfig.put("parser", "typescript"); if (replaced != null) { - getProject().getLogger().warn("overriding parser option to 'typescript'. Was set to '{}'", replaced); + getProject().getLogger().warn("overriding parser option to 'typescript'. Was set to '{}'", + replaced); } } } } + public TypescriptEslintConfig eslint() { + return eslint(EslintFormatterStep.defaultDevDependenciesForTypescript()); + } + + public TypescriptEslintConfig eslint(String version) { + return eslint(EslintFormatterStep.defaultDevDependenciesTypescriptWithEslint(version)); + } + + public TypescriptEslintConfig eslint(Map devDependencies) { + TypescriptEslintConfig eslint = new TypescriptEslintConfig(devDependencies); + addStep(eslint.createStep()); + return eslint; + } + + public class TypescriptEslintConfig extends EslintBaseConfig { + + @Nullable + Object typescriptConfigFilePath = null; + + public TypescriptEslintConfig(Map devDependencies) { + super(getProject(), TypescriptExtension.this::replaceStep, devDependencies); + } + + public TypescriptEslintConfig tsconfigFile(Object path) { + this.typescriptConfigFilePath = requireNonNull(path); + replaceStep(); + return this; + } + + public FormatterStep createStep() { + final Project project = getProject(); + + return EslintFormatterStep.create(devDependencies, provisioner(), project.getProjectDir(), + project.getLayout().getBuildDirectory().getAsFile().get(), npmModulesCacheOrNull(), + new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), + Arrays.asList(project.getProjectDir(), project.getRootDir())), + eslintConfig()); + } + + protected EslintConfig eslintConfig() { + return new EslintTypescriptConfig(configFilePath != null ? getProject().file(configFilePath) : null, + configJs, typescriptConfigFilePath != null ? getProject().file(typescriptConfigFilePath) : null); + } + } + + /** + * Defaults to downloading the default Biome version from the network. To work + * offline, you can specify the path to the Biome executable via + * {@code biome().pathToExe(...)}. + */ + public BiomeTs biome() { + return biome(null); + } + + /** Downloads the given Biome version from the network. */ + public BiomeTs biome(String version) { + var biomeConfig = new BiomeTs(version); + addStep(biomeConfig.createStep()); + return biomeConfig; + } + + /** + * Biome formatter step for TypeScript. + */ + public class BiomeTs extends BiomeStepConfig { + /** + * Creates a new Biome formatter step config for formatting TypeScript files. + * Unless overwritten, the given Biome version is downloaded from the network. + * + * @param version Biome version to use. + */ + public BiomeTs(String version) { + super(getProject(), TypescriptExtension.this::replaceStep, BiomeFlavor.BIOME, version); + } + + @Override + protected String getLanguage() { + return "ts?"; + } + + @Override + protected BiomeTs getThis() { + return this; + } + } + @Override protected void setupTask(SpotlessTask task) { - // defaults to all typescript files if (target == null) { throw noDefaultTargetException(); } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java new file mode 100644 index 0000000000..c0872e8fb9 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2023 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.util.Collections; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.yaml.JacksonYamlConfig; +import com.diffplug.spotless.yaml.JacksonYamlStep; + +public class YamlExtension extends FormatExtension { + static final String NAME = "yaml"; + + @Inject + public YamlExtension(SpotlessExtension spotless) { + super(spotless); + } + + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + throw noDefaultTargetException(); + } + super.setupTask(task); + } + + public JacksonYamlGradleConfig jackson() { + return new JacksonYamlGradleConfig(this); + } + + public class JacksonYamlGradleConfig extends AJacksonGradleConfig { + protected JacksonYamlConfig jacksonConfig; + + public JacksonYamlGradleConfig(JacksonYamlConfig jacksonConfig, FormatExtension formatExtension) { + super(jacksonConfig, formatExtension); + + this.jacksonConfig = jacksonConfig; + + formatExtension.addStep(createStep()); + } + + public JacksonYamlGradleConfig(FormatExtension formatExtension) { + this(new JacksonYamlConfig(), formatExtension); + } + + /** + * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature + */ + public JacksonYamlGradleConfig yamlFeature(String feature, boolean toggle) { + this.jacksonConfig.appendYamlFeatureToToggle(Collections.singletonMap(feature, toggle)); + formatExtension.replaceStep(createStep()); + return this; + } + + @Override + public JacksonYamlGradleConfig self() { + return this; + } + + // 'final' as it is called in the constructor + @Override + protected final FormatterStep createStep() { + return JacksonYamlStep.create(jacksonConfig, version, provisioner()); + } + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BiomeIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BiomeIntegrationTest.java new file mode 100644 index 0000000000..0a8dfe7022 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BiomeIntegrationTest.java @@ -0,0 +1,427 @@ +/* + * Copyright 2023-2024 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.owasp.encoder.Encode; + +class BiomeIntegrationTest extends GradleIntegrationHarness { + /** + * Tests that biome can be used as a JSON formatting step, using biome 1.8.3 + * which + * requires opt-in. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asCssStepExperimental() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " css {", + " target '**/*.css'", + " biome('1.8.3').configPath('configs')", + " }", + "}"); + setFile("biome_test.css").toResource("biome/css/fileBefore.css"); + setFile("configs/biome.json").toResource("biome/config/css-enabled.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.css").sameAsResource("biome/css/fileAfter.css"); + } + + /** + * Tests that biome can be used as a JSON formatting step, using biome 1.9.0 + * which + * does not require opt-in. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asCssStepStable() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " css {", + " target '**/*.css'", + " biome('1.9.0')", + " }", + "}"); + setFile("biome_test.css").toResource("biome/css/fileBefore.css"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.css").sameAsResource("biome/css/fileAfter.css"); + } + + /** + * Tests that biome can be used as a generic formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asGenericStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + } + + /** + * Tests that biome can be used as a JavaScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJavaScriptStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target '**/*.js'", + " biome('1.2.0')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + } + + /** + * Tests that biome can be used as a JSON formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJsonStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target '**/*.json'", + " biome('1.2.0')", + " }", + "}"); + setFile("biome_test.json").toResource("biome/json/fileBefore.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.json").sameAsResource("biome/json/fileAfter.json"); + } + + /** + * Tests that biome can be used as a TypeScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asTypeScriptStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target '**/*.ts'", + " biome('1.2.0')", + " }", + "}"); + setFile("biome_test.ts").toResource("biome/ts/fileBefore.ts"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.ts").sameAsResource("biome/ts/fileAfter.ts"); + } + + /** + * Tests that the language can be specified for the generic format step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void canSetLanguageForGenericStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.nosj'", + " biome('1.2.0').language('json')", + " }", + "}"); + setFile("biome_test.nosj").toResource("biome/json/fileBefore.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.nosj").sameAsResource("biome/json/fileAfter.json"); + } + + /** + * Tests that an absolute config path can be specified. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathAbsolute() throws Exception { + var path = newFile("configs").getAbsolutePath(); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').configPath('" + Encode.forJava(path) + "')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-120.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the biome.json config file can be + * specified. Uses a config file with a line width of 120. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth120() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').configPath('configs')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-120.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the biome.json config file can be + * specified. Uses a config file with a line width of 80. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth80() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').configPath('configs')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-80.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter80.js"); + } + + /** + * Tests that the download directory can be an absolute path. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirAbsolute() throws Exception { + var path = newFile("target/bin/biome").getAbsoluteFile().toString(); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').downloadDir('" + Encode.forJava(path) + "')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/biome").exists() || newFile("target/bin/biome").list().length == 0); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/biome").list().length); + } + + /** + * Tests that the download directory can be changed to a path relative to the + * project's base directory. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirRelative() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').downloadDir('target/bin/biome')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/biome").exists() || newFile("target/bin/biome").list().length == 0); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/biome").list().length); + } + + /** + * Tests that the build fails when given Biome executable does not exist. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenExeNotFound() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').pathToExe('biome/is/missing')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail(); + assertThat(spotlessApply.getOutput()).contains("Build failed with an exception"); + assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); + assertThat(spotlessApply.getOutput()).contains("Execution failed for task ':spotlessMybiome'"); + assertThat(spotlessApply.getOutput()).contains("Biome executable does not exist"); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenNotParseable() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'mybiome', {", + " target '**/*.js'", + " biome('1.2.0').language('json')", + " }", + "}"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(spotlessApply.getOutput()).contains("spotlessMybiomeApply FAILED"); + assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); + assertThat(spotlessApply.getOutput()).contains("Format with errors is disabled."); + assertThat(spotlessApply.getOutput()).contains("biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException)"); + } + + /** + * Biome is hard-coded to ignore certain files, such as package.json. Since + * version 1.5.0, + * the biome CLI does not output any formatted code anymore, whereas previously + * it printed + * the input as-is. This tests checks that when the biome formatter outputs an + * empty string, + * the contents of the file to format are used instead. + * + * @throws Exception When a test failure occurs. + */ + @Test + void preservesIgnoredFiles() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target '**/*.json'", + " biome('1.5.0')", + " }", + "}"); + setFile("package.json").toResource("biome/json/packageBefore.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("package.json").sameAsResource("biome/json/packageAfter.json"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java new file mode 100644 index 0000000000..be26cc8e71 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022-2024 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; + +import com.diffplug.spotless.tag.BufTest; + +@BufTest +class BufIntegrationTest extends GradleIntegrationHarness { + @Test + void bufLarge() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " protobuf {", + " buf()", + " }", + "}"); + setFile("buf.proto").toResource("protobuf/buf/buf_large.proto"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf_large.proto.clean"); + } + + @Test + void buf() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " protobuf {", + " buf()", + " }", + "}"); + setFile("buf.proto").toResource("protobuf/buf/buf.proto"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf.proto.clean"); + } + + @Test + void bufWithLicense() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " protobuf {", + " buf()", + " licenseHeader '/* (C) 2022 */'", + " }", + "}"); + setFile("license.proto").toResource("protobuf/buf/license.proto"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("license.proto").sameAsResource("protobuf/buf/license.proto.clean"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BumpThisNumberIfACustomStepChangesTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BumpThisNumberIfACustomStepChangesTest.java index f959226032..25ed3cab8b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BumpThisNumberIfACustomStepChangesTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BumpThisNumberIfACustomStepChangesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,38 @@ import java.io.IOException; +import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.Test; -class BumpThisNumberIfACustomStepChangesTest extends GradleIntegrationHarness { +abstract class BumpThisNumberIfACustomStepChangesTest extends GradleIntegrationHarness { + private boolean useConfigCache; + + BumpThisNumberIfACustomStepChangesTest(boolean useConfigCache) { + this.useConfigCache = useConfigCache; + } + + static class WithConfigCache extends BumpThisNumberIfACustomStepChangesTest { + WithConfigCache() { + super(true); + } + } + + static class WithoutConfigCache extends BumpThisNumberIfACustomStepChangesTest { + WithoutConfigCache() { + super(false); + } + } + + @Override + public GradleRunner gradleRunner() throws IOException { + if (useConfigCache) { + setFile("gradle.properties").toLines("org.gradle.unsafe.configuration-cache=true", + "org.gradle.configuration-cache=true"); + return super.gradleRunner().withGradleVersion(GradleVersionSupport.CUSTOM_STEPS.version); + } else { + return super.gradleRunner(); + } + } private void writeBuildFile(String toInsert) throws IOException { setFile("build.gradle").toLines( @@ -40,7 +69,7 @@ private void writeContentWithBadFormatting() throws IOException { } @Override - protected void applyIsUpToDate(boolean upToDate) throws IOException { + public void applyIsUpToDate(boolean upToDate) throws IOException { super.applyIsUpToDate(upToDate); assertFile("README.md").hasContent("abc"); } @@ -51,7 +80,12 @@ void customRuleNeverUpToDate() throws IOException { writeContentWithBadFormatting(); applyIsUpToDate(false); checkIsUpToDate(false); - checkIsUpToDate(false); + if (useConfigCache) { + // if the config cache is in-effect, then it's okay for custom rules to become "up-to-date" + checkIsUpToDate(true); + } else { + checkIsUpToDate(false); + } } @Test diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CleanthatJavaIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CleanthatJavaIntegrationTest.java new file mode 100644 index 0000000000..ad6fff9112 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/CleanthatJavaIntegrationTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022-2023 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 CleanthatJavaIntegrationTest extends GradleIntegrationHarness { + @Test + void integration() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " cleanthat()", + " .sourceCompatibility('11')", + " .addMutators(['LiteralsFirstInComparisons', 'OptionalNotEmpty'])", + " }", + "}"); + + setFile("test.java").toResource("java/cleanthat/MultipleMutators.dirty.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.java").sameAsResource("java/cleanthat/MultipleMutators.clean.test"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigAvoidanceTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigAvoidanceTest.java index d5422a3631..bd521bd864 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigAvoidanceTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigAvoidanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ void noConfigOnHelp() throws IOException { "apply plugin: 'java'", "spotless {", " java {", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}", "", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigurationCacheTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigurationCacheTest.java index 14b6de7702..e76585fe54 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigurationCacheTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ConfigurationCacheTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,15 @@ import java.io.IOException; -import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.Test; public class ConfigurationCacheTest extends GradleIntegrationHarness { @Override - protected GradleRunner gradleRunner() throws IOException { + public GradleRunner gradleRunner() throws IOException { setFile("gradle.properties").toContent("org.gradle.unsafe.configuration-cache=true"); - return super.gradleRunner().withGradleVersion(GradleVersionSupport.CONFIGURATION_CACHE.version); + setFile("settings.gradle").toContent("enableFeaturePreview(\"STABLE_CONFIGURATION_CACHE\")"); + return super.gradleRunner().withGradleVersion(GradleVersionSupport.STABLE_CONFIGURATION_CACHE.version); } @Test @@ -38,7 +38,7 @@ public void helpConfigures() throws IOException { "apply plugin: 'java'", "spotless {", " java {", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); gradleRunner().withArguments("help").build(); @@ -54,7 +54,7 @@ public void helpConfiguresIfTasksAreCreated() throws IOException { "apply plugin: 'java'", "spotless {", " java {", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}", "tasks.named('spotlessJavaApply').get()"); @@ -62,7 +62,7 @@ public void helpConfiguresIfTasksAreCreated() throws IOException { } @Test - public void jvmLocalCache() throws IOException { + public void multipleRuns() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -71,7 +71,7 @@ public void jvmLocalCache() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); @@ -89,10 +89,5 @@ public void jvmLocalCache() throws IOException { gradleRunner().withArguments("spotlessCheck").buildAndFail(); gradleRunner().withArguments("spotlessApply").build(); assertFile("test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); - - BuildResult failure = gradleRunner().withDebug(true).withArguments("spotlessApply", "--stacktrace").buildAndFail(); - failure.getOutput().contains("Spotless daemon-local cache is stale. Regenerate the cache with\n" + - " rm -rf .gradle/configuration-cache\n" + - "For more information see #123\n"); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java index c46d398bf8..24b2a8504d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/DiffMessageFormatterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.regex.Pattern; import org.assertj.core.api.Assertions; import org.gradle.api.Project; @@ -31,11 +30,11 @@ import com.diffplug.common.base.StringPrinter; import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.TestProvisioner; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; +import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; class DiffMessageFormatterTest extends ResourceHarness { @@ -62,7 +61,7 @@ public BuildServiceParameters.None getParameters() { private SpotlessTaskImpl createFormatTask(String name) { SpotlessTaskImpl task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name), SpotlessTaskImpl.class); task.init(taskService); - task.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); + task.setLineEndingsPolicy(project.provider(LineEnding.UNIX::createPolicy)); task.setTarget(Collections.singletonList(file)); return task; } @@ -100,7 +99,7 @@ private Bundle create(File... files) throws IOException { private Bundle create(List files) throws IOException { Bundle bundle = new Bundle("underTest"); - bundle.task.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); + bundle.task.setLineEndingsPolicy(bundle.project.provider(LineEnding.UNIX::createPolicy)); bundle.task.setTarget(files); return bundle; } @@ -135,13 +134,23 @@ void lineEndingProblem() throws Exception { " +C\\n"); } + @Test + void customRunToFixMessage() throws Exception { + Bundle task = create(setFile("testFile").toContent("A\r\nB\r\nC\r\n")); + String customMessage = "Formatting issues detected, please read automatic-code-formatting.txt and correct."; + task.check.getRunToFixMessage().set(customMessage); + + String msg = task.checkFailureMsg(); + + String firstLine = "The following files had format violations:\n"; + String lastLine = "\n" + customMessage; + Assertions.assertThat(msg).startsWith(firstLine).endsWith(lastLine); + } + @Test void whitespaceProblem() throws Exception { Bundle spotless = create(setFile("testFile").toContent("A \nB\t\nC \n")); - spotless.task.addStep(FormatterStep.createNeverUpToDate("trimTrailing", input -> { - Pattern pattern = Pattern.compile("[ \t]+$", Pattern.UNIX_LINES | Pattern.MULTILINE); - return pattern.matcher(input).replaceAll(""); - })); + spotless.task.setSteps(List.of(TrimTrailingWhitespaceStep.create())); assertCheckFailure(spotless, " testFile", " @@ -1,3 +1,3 @@", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java index d1ced01609..e083161a09 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,11 @@ import java.util.Arrays; import java.util.List; -import org.assertj.core.api.Assertions; import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.Test; -import com.diffplug.common.base.CharMatcher; -import com.diffplug.common.base.Splitter; -import com.diffplug.spotless.LineEnding; +import com.diffplug.selfie.Selfie; +import com.diffplug.selfie.StringSelfie; /** Tests the desired behavior from https://github.com/diffplug/spotless/issues/46. */ class ErrorShouldRethrowTest extends GradleIntegrationHarness { @@ -41,9 +38,9 @@ private void writeBuild(String... toInsert) throws IOException { lines.add(" format 'misc', {"); lines.add(" lineEndings 'UNIX'"); lines.add(" target file('README.md')"); - lines.add(" custom 'no swearing', {"); + lines.add(" custom 'noSwearingStep', {"); lines.add(" if (it.toLowerCase(Locale.ROOT).contains('fubar')) {"); - lines.add(" throw new RuntimeException('No swearing!');"); + lines.add(" throw com.diffplug.spotless.Lint.atUndefinedLine('swearing', 'No swearing!').shortcut();"); lines.add(" }"); lines.add(" }"); lines.addAll(Arrays.asList(toInsert)); @@ -56,7 +53,7 @@ void passesIfNoException() throws Exception { " } // format", "} // spotless"); setFile("README.md").toContent("This code is fun."); - runWithSuccess("> Task :spotlessMisc"); + expectSuccess(); } @Test @@ -65,11 +62,16 @@ void anyExceptionShouldFail() throws Exception { " } // format", "} // spotless"); setFile("README.md").toContent("This code is fubar."); - runWithFailure( - "> Task :spotlessMisc FAILED\n" + - "Step 'no swearing' found problem in 'README.md':\n" + - "No swearing!\n" + - "java.lang.RuntimeException: No swearing!"); + expectFailureAndConsoleToBe().toBe("> Task :spotlessMisc", + "> Task :spotlessMiscCheck FAILED", + "", + "FAILURE: Build failed with an exception.", + "", + "* What went wrong:", + "Execution failed for task ':spotlessMiscCheck'.", + "> There were 1 lint error(s), they must be fixed or suppressed.", + " README.md:LINE_UNDEFINED noSwearingStep(swearing) No swearing!", + " Resolve these lints or suppress with `suppressLintsFor`"); } @Test @@ -79,18 +81,17 @@ void unlessEnforceCheckIsFalse() throws Exception { " enforceCheck false", "} // spotless"); setFile("README.md").toContent("This code is fubar."); - runWithSuccess("> Task :processResources NO-SOURCE"); + expectSuccess(); } @Test void unlessExemptedByStep() throws Exception { writeBuild( - " ignoreErrorForStep 'no swearing'", + " ignoreErrorForStep 'noSwearingStep'", " } // format", "} // spotless"); setFile("README.md").toContent("This code is fubar."); - runWithSuccess("> Task :spotlessMisc\n" + - "Unable to apply step 'no swearing' to 'README.md'"); + expectSuccess(); } @Test @@ -100,8 +101,7 @@ void unlessExemptedByPath() throws Exception { " } // format", "} // spotless"); setFile("README.md").toContent("This code is fubar."); - runWithSuccess("> Task :spotlessMisc\n" + - "Unable to apply step 'no swearing' to 'README.md'"); + expectSuccess(); } @Test @@ -112,33 +112,29 @@ void failsIfNeitherStepNorFileExempted() throws Exception { " } // format", "} // spotless"); setFile("README.md").toContent("This code is fubar."); - runWithFailure("> Task :spotlessMisc FAILED\n" + - "Step 'no swearing' found problem in 'README.md':\n" + - "No swearing!\n" + - "java.lang.RuntimeException: No swearing!"); + expectFailureAndConsoleToBe().toBe("> Task :spotlessMisc", + "> Task :spotlessMiscCheck FAILED", + "", + "FAILURE: Build failed with an exception.", + "", + "* What went wrong:", + "Execution failed for task ':spotlessMiscCheck'.", + "> There were 1 lint error(s), they must be fixed or suppressed.", + " README.md:LINE_UNDEFINED noSwearingStep(swearing) No swearing!", + " Resolve these lints or suppress with `suppressLintsFor`"); } - private void runWithSuccess(String expectedToStartWith) throws Exception { - BuildResult result = gradleRunner().withArguments("check").build(); - assertResultAndMessages(result, TaskOutcome.SUCCESS, expectedToStartWith); + private void expectSuccess() throws Exception { + gradleRunner().withArguments("check", "--stacktrace").build(); } - private void runWithFailure(String expectedToStartWith) throws Exception { + private StringSelfie expectFailureAndConsoleToBe() throws Exception { BuildResult result = gradleRunner().withArguments("check").buildAndFail(); - assertResultAndMessages(result, TaskOutcome.FAILED, expectedToStartWith); - } - - private void assertResultAndMessages(BuildResult result, TaskOutcome outcome, String expectedToStartWith) { String output = result.getOutput(); int register = output.indexOf(":spotlessInternalRegisterDependencies"); int firstNewlineAfterThat = output.indexOf('\n', register + 1); - String useThisToMatch = output.substring(firstNewlineAfterThat); - - int numNewlines = CharMatcher.is('\n').countIn(expectedToStartWith); - List actualLines = Splitter.on('\n').splitToList(LineEnding.toUnix(useThisToMatch.trim())); - String actualStart = String.join("\n", actualLines.subList(0, numNewlines + 1)); - Assertions.assertThat(actualStart).isEqualTo(expectedToStartWith); - Assertions.assertThat(outcomes(result, outcome).size() + outcomes(result, TaskOutcome.UP_TO_DATE).size() + outcomes(result, TaskOutcome.NO_SOURCE).size()) - .isEqualTo(outcomes(result).size()); + int firstTry = output.indexOf("\n* Try:"); + String useThisToMatch = output.substring(firstNewlineAfterThat, firstTry).trim(); + return Selfie.expectSelfie(useThisToMatch); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FilePermissionsTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FilePermissionsTest.java index a5ce2d11cd..d342348a5b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FilePermissionsTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FilePermissionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 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 spotlessApplyShouldPreservePermissions() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java new file mode 100644 index 0000000000..6264c0c230 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FlexmarkExtensionTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 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 FlexmarkExtensionTest extends GradleIntegrationHarness { + @Test + void integration() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " flexmark {", + " target '*.md'", + " flexmark()", + " }", + "}"); + setFile("markdown_test.md").toResource("markdown/flexmark/FlexmarkUnformatted.md"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkFormatted.md"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java index 8985cd7a14..0d10cf54e3 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,17 @@ import java.io.File; import java.util.Collections; +import java.util.List; import org.gradle.api.Project; import org.gradle.api.services.BuildServiceParameters; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.TestProvisioner; +import com.diffplug.spotless.generic.ReplaceStep; class FormatTaskTest extends ResourceHarness { private SpotlessTaskImpl spotlessTask; @@ -35,7 +36,7 @@ class FormatTaskTest extends ResourceHarness { void createTask() { Project project = TestProvisioner.gradleProject(rootFolder()); spotlessTask = project.getTasks().create("spotlessTaskUnderTest", SpotlessTaskImpl.class); - spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); + spotlessTask.setLineEndingsPolicy(project.provider(LineEnding.UNIX::createPolicy)); spotlessTask.init(GradleIntegrationHarness.providerOf(new SpotlessTaskService() { @Override public BuildServiceParameters.None getParameters() { @@ -47,7 +48,7 @@ public BuildServiceParameters.None getParameters() { @Test void testLineEndings() throws Exception { File testFile = setFile("testFile").toContent("\r\n"); - File outputFile = new File(spotlessTask.getOutputDirectory(), "testFile"); + File outputFile = new File(spotlessTask.getCleanDirectory(), "testFile"); spotlessTask.setTarget(Collections.singleton(testFile)); Tasks.execute(spotlessTask); @@ -58,10 +59,10 @@ void testLineEndings() throws Exception { @Test void testStep() throws Exception { File testFile = setFile("testFile").toContent("apple"); - File outputFile = new File(spotlessTask.getOutputDirectory(), "testFile"); + File outputFile = new File(spotlessTask.getCleanDirectory(), "testFile"); spotlessTask.setTarget(Collections.singleton(testFile)); - spotlessTask.addStep(FormatterStep.createNeverUpToDate("double-p", content -> content.replace("pp", "p"))); + spotlessTask.setSteps(List.of(ReplaceStep.create("double-p", "pp", "p"))); Tasks.execute(spotlessTask); assertFile(outputFile).hasContent("aple"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GherkinExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GherkinExtensionTest.java new file mode 100644 index 0000000000..b78094cf18 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GherkinExtensionTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2023 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; + +public class GherkinExtensionTest extends GradleIntegrationHarness { + @Test + public void defaultFormatting() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " gherkin {", + " target 'examples/**/*.feature'", + " gherkinUtils()", + " }", + "}"); + setFile("src/main/resources/example.feature").toResource("gherkin/minimalBefore.feature"); + setFile("examples/main/resources/example.feature").toResource("gherkin/minimalBefore.feature"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.feature").sameAsResource("gherkin/minimalBefore.feature"); + assertFile("examples/main/resources/example.feature").sameAsResource("gherkin/minimalAfter.feature"); + } + +} 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 51f4808259..e4c1d31615 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import com.diffplug.spotless.ClearGitConfig; + +@ClearGitConfig class GitRatchetGradleTest extends GradleIntegrationHarness { private static final String TEST_PATH = "src/markdown/test.md"; @@ -49,12 +52,13 @@ private Git initRepo() throws IllegalStateException, GitAPIException, IOExceptio } @Override - protected GradleRunner gradleRunner() throws IOException { - return super.gradleRunner().withGradleVersion(GradleVersionSupport.CONFIGURATION_CACHE.version); + public GradleRunner gradleRunner() throws IOException { + return super.gradleRunner().withGradleVersion(GradleVersionSupport.CUSTOM_STEPS.version); } @ParameterizedTest - @ValueSource(ints = {0, 1}) + //@ValueSource(ints = {0, 1}) // TODO: this is a flaky configuration cache issue that started with Gradle 8.5 + @ValueSource(ints = {0}) void singleProjectExhaustive(int useConfigCache) throws Exception { try (Git git = initRepo()) { if (useConfigCache == 1) { @@ -153,7 +157,8 @@ private BuildResultAssertion assertFail(String... tasks) throws Exception { private static final String BASELINE_DIRTY = "4cfc3358ccbf186738b82a60276b1e5306bc3870"; @ParameterizedTest - @ValueSource(ints = {0, 1}) + //@ValueSource(ints = {0, 1}) // TODO: this is a flaky configuration cache issue that started with Gradle 8.5 + @ValueSource(ints = {0}) void multiProject(int useConfigCache) throws Exception { try (Git git = initRepo()) { if (useConfigCache == 1) { @@ -266,10 +271,9 @@ 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(); - RevCommit commit = git.commit().setMessage("baseline") + return git.commit().setMessage("baseline") .setCommitter(emptyPerson) .setAuthor(emptyPerson) .call(); - return commit; } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoGradleTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoGradleTest.java new file mode 100644 index 0000000000..94bd345854 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GoGradleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2024 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; + +import com.diffplug.spotless.tag.GofmtTest; + +@GofmtTest +class GoGradleTest extends GradleIntegrationHarness { + @Test + void gofmt() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " go {", + " target 'src/**/*.go'", + " gofmt(\"go1.21.5\")", + " }", + "}"); + setFile("src/test.go").toResource("go/gofmt/go.dirty"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/test.go").sameAsResource("go/gofmt/go.clean"); + } +} 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 1f5cb6af05..e7fc9d4b74 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-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ void integration() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.2')", + " googleJavaFormat('1.17.0')", " }", "}"); @@ -41,8 +41,60 @@ void integration() throws IOException { checkRunsThenUpToDate(); replace("build.gradle", - "googleJavaFormat('1.2')", - "googleJavaFormat('1.3')"); + "googleJavaFormat('1.17.0')", + "googleJavaFormat()"); + checkRunsThenUpToDate(); + } + + @Test + void integrationWithReorderImports() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0').aosp().reorderImports(true)", + " }", + "}"); + + setFile("test.java").toResource("java/googlejavaformat/JavaWithReorderImportsUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.java").sameAsResource("java/googlejavaformat/JavaWithReorderImportsEnabledFormatted.test"); + + checkRunsThenUpToDate(); + replace("build.gradle", + "googleJavaFormat('1.17.0')", + "googleJavaFormat()"); + checkRunsThenUpToDate(); + } + + @Test + void integrationWithSkipJavadocFormatting() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0').skipJavadocFormatting()", + " }", + "}"); + + setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.java").sameAsResource("java/googlejavaformat/JavaCodeFormattedSkipJavadocFormatting.test"); + + checkRunsThenUpToDate(); + replace("build.gradle", + "googleJavaFormat('1.17.0')", + "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 98b1e1cbf9..100bdefc50 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.List; import java.util.ListIterator; +import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -29,6 +30,7 @@ import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.BeforeEach; import com.diffplug.common.base.Errors; @@ -41,34 +43,58 @@ public class GradleIntegrationHarness extends ResourceHarness { public enum GradleVersionSupport { - JRE_11("5.0"), MINIMUM(SpotlessPlugin.MINIMUM_GRADLE), + JRE_11("5.0"), MINIMUM(SpotlessPlugin.VER_GRADLE_min), CUSTOM_STEPS(SpotlessPlugin.VER_GRADLE_minVersionForCustom), // technically, this API exists in 6.5, but the flags for it change in 6.6, so we build to that - CONFIGURATION_CACHE("6.6"); + CONFIGURATION_CACHE("6.6"), + // https://docs.gradle.org/7.5/userguide/configuration_cache.html#config_cache:stable + STABLE_CONFIGURATION_CACHE("7.5"); final String version; GradleVersionSupport(String version) { + String minVersionForRunningJRE; switch (Jvm.version()) { + case 24: + // TODO: https://docs.gradle.org/current/userguide/compatibility.html + case 23: + minVersionForRunningJRE = "8.10"; + break; + case 22: + minVersionForRunningJRE = "8.8"; + break; + case 21: + minVersionForRunningJRE = "8.5"; + break; case 20: + minVersionForRunningJRE = "8.3"; + break; case 19: + minVersionForRunningJRE = "7.6"; + break; case 18: - // TODO: https://docs.gradle.org/current/userguide/compatibility.html + minVersionForRunningJRE = "7.5"; + break; case 17: - this.version = "7.3"; + minVersionForRunningJRE = "7.3"; break; case 16: - this.version = "7.0"; + minVersionForRunningJRE = "7.0"; break; case 15: - this.version = "6.7"; + minVersionForRunningJRE = "6.7"; break; case 14: - this.version = "6.3"; + minVersionForRunningJRE = "6.3"; break; default: - this.version = version; + minVersionForRunningJRE = null; break; } + if (minVersionForRunningJRE != null && GradleVersion.version(minVersionForRunningJRE).compareTo(GradleVersion.version(version)) > 0) { + this.version = minVersionForRunningJRE; + } else { + this.version = version; + } } } @@ -79,12 +105,12 @@ public static Provider providerOf(T value) { /** * Each test gets its own temp folder, and we create a gradle * build there and run it. - * + *

* Because those test folders don't have a .gitattributes file, * git (on windows) will default to \r\n. So now if you read a * test file from the spotless test resources, and compare it * to a build result, the line endings won't match. - * + *

* By sticking this .gitattributes file into the test directory, * we ensure that the default Spotless line endings policy of * GIT_ATTRIBUTES will use \n, so that tests match the test @@ -95,19 +121,49 @@ void gitAttributes() throws IOException { setFile(".gitattributes").toContent("* text eol=lf"); } - protected GradleRunner gradleRunner() throws IOException { + public GradleRunner gradleRunner() throws IOException { + GradleVersionSupport version; + if (newFile("build.gradle").exists() && read("build.gradle").contains("custom")) { + version = GradleVersionSupport.CUSTOM_STEPS; + } else { + version = GradleVersionSupport.MINIMUM; + } return GradleRunner.create() - .withGradleVersion(GradleVersionSupport.MINIMUM.version) + .withGradleVersion(version.version) .withProjectDir(rootFolder()) + .withTestKitDir(getTestKitDir()) .withPluginClasspath(); } /** Dumps the complete file contents of the folder to the console. */ - protected String getContents() throws IOException { + public String getContents() { return getContents(subPath -> !subPath.startsWith(".gradle")); } - protected String getContents(Predicate subpathsToInclude) throws IOException { + public String getContents(Predicate subpathsToInclude) { + return StringPrinter.buildString(printer -> Errors.rethrow().run(() -> iterateFiles(subpathsToInclude, (subpath, file) -> { + printer.println("### " + subpath + " ###"); + try { + printer.println(read(subpath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }))); + } + + /** Dumps the filtered file listing of the folder to the console. */ + public String listFiles(Predicate subpathsToInclude) { + return StringPrinter.buildString(printer -> iterateFiles(subpathsToInclude, (subPath, file) -> { + printer.println(subPath + " [" + getFileAttributes(file) + "]"); + })); + } + + /** Dumps the file listing of the folder to the console. */ + public String listFiles() { + return listFiles(subPath -> !subPath.startsWith(".gradle")); + } + + public void iterateFiles(Predicate subpathsToInclude, BiConsumer consumer) { TreeDef treeDef = TreeDef.forFile(Errors.rethrow()); List files = TreeStream.depthFirst(treeDef, rootFolder()) .filter(File::isFile) @@ -115,28 +171,29 @@ protected String getContents(Predicate subpathsToInclude) throws IOExcep ListIterator iterator = files.listIterator(files.size()); int rootLength = rootFolder().getAbsolutePath().length() + 1; - return StringPrinter.buildString(printer -> Errors.rethrow().run(() -> { - while (iterator.hasPrevious()) { - File file = iterator.previous(); - String subPath = file.getAbsolutePath().substring(rootLength); - if (subpathsToInclude.test(subPath)) { - printer.println("### " + subPath + " ###"); - printer.println(read(subPath)); - } + while (iterator.hasPrevious()) { + File file = iterator.previous(); + String subPath = file.getAbsolutePath().substring(rootLength); + if (subpathsToInclude.test(subPath)) { + consumer.accept(subPath, file); } - })); + } + } + + public String getFileAttributes(File file) { + return (file.canRead() ? "r" : "-") + (file.canWrite() ? "w" : "-") + (file.canExecute() ? "x" : "-"); } - protected void checkRunsThenUpToDate() throws IOException { + public void checkRunsThenUpToDate() throws IOException { checkIsUpToDate(false); checkIsUpToDate(true); } - protected void applyIsUpToDate(boolean upToDate) throws IOException { + public void applyIsUpToDate(boolean upToDate) throws IOException { taskIsUpToDate("spotlessApply", upToDate); } - protected void checkIsUpToDate(boolean upToDate) throws IOException { + public void checkIsUpToDate(boolean upToDate) throws IOException { taskIsUpToDate("spotlessCheck", upToDate); } @@ -158,13 +215,13 @@ private void taskIsUpToDate(String task, boolean upToDate) throws IOException { } } - protected static List outcomes(BuildResult build, TaskOutcome outcome) { + public static List outcomes(BuildResult build, TaskOutcome outcome) { return build.taskPaths(outcome).stream() .filter(s -> !s.equals(":spotlessInternalRegisterDependencies")) .collect(Collectors.toList()); } - protected static List outcomes(BuildResult build) { + public static List outcomes(BuildResult build) { return build.getTasks().stream() .filter(t -> !t.getPath().equals(":spotlessInternalRegisterDependencies")) .collect(Collectors.toList()); @@ -177,4 +234,12 @@ static String buildResultToString(BuildResult result) { } }); } + + private static File getTestKitDir() { + String gradleUserHome = System.getenv("GRADLE_USER_HOME"); + if (gradleUserHome == null || gradleUserHome.isEmpty()) { + gradleUserHome = new File(System.getProperty("user.home"), ".gradle").getAbsolutePath(); + } + return new File(gradleUserHome, "testkit"); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyExtensionTest.java index b9c7d2f66e..7f87d8793c 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,6 +88,27 @@ void excludeJavaWithCustomTarget() throws IOException { assertThat(error).hasMessageContaining("'excludeJava' is not supported"); } + @Test + void removeSemicolons() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "apply plugin: 'groovy'", + "", + "spotless {", + " groovy {", + " removeSemicolons()", + " }", + "}"); + + String withSemicolons = getTestResource("groovy/removeSemicolons/GroovyCodeWithSemicolons.test"); + String withoutSemicolons = getTestResource("groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test"); + setFile("src/main/groovy/test.groovy").toContent(withSemicolons); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/groovy/test.groovy").hasContent(withoutSemicolons); + } + @Test void groovyPluginMissingCheck() throws IOException { setFile("build.gradle").toLines( @@ -103,7 +124,7 @@ void groovyPluginMissingCheck() throws IOException { Throwable error = assertThrows(Throwable.class, () -> gradleRunner().withArguments("spotlessApply").build()); - assertThat(error).hasMessageContaining("must apply the groovy plugin before"); + assertThat(error).hasMessageContaining("You must either specify 'target' manually or apply the 'groovy' plugin."); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyGradleExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyGradleExtensionTest.java index 7f4d593a9c..e94d269644 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyGradleExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GroovyGradleExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,24 +17,20 @@ import java.io.IOException; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import com.diffplug.common.base.StringPrinter; -class GroovyGradleExtensionTest extends GradleIntegrationHarness { +/** + * We test GroovyGradleExtension only behaviors here. + */ +class GroovyGradleExtensionTest extends GroovyExtensionTest { private static final String HEADER = "//My tests header"; - @Test - void defaultTarget() throws IOException { - testTarget(true); - } - - @Test - void customTarget() throws IOException { - testTarget(false); - } - - private void testTarget(boolean useDefaultTarget) throws IOException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testTarget(boolean useDefaultTarget) throws IOException { String target = useDefaultTarget ? "" : "target 'other.gradle'"; String buildContent = StringPrinter.buildStringFromLines( "plugins {", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java index f28b9f9820..c38484384f 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,19 @@ import java.io.IOException; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import com.diffplug.common.base.StringPrinter; import com.diffplug.common.io.Files; +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class IdeHookTest extends GradleIntegrationHarness { private String output, error; private File dirty, clean, diverge, outofbounds; @@ -40,11 +45,11 @@ void before() throws IOException { "spotless {", " format 'misc', {", " target 'DIRTY.md', 'CLEAN.md'", - " custom 'lowercase', { str -> str.toLowerCase(Locale.ROOT) }", + " addStep com.diffplug.spotless.TestingOnly.lowercase()", " }", " format 'diverge', {", " target 'DIVERGE.md'", - " custom 'diverge', { str -> str + ' ' }", + " addStep com.diffplug.spotless.TestingOnly.diverge()", " }", "}"); dirty = new File(rootFolder(), "DIRTY.md"); @@ -57,12 +62,16 @@ void before() throws IOException { Files.write("ABC".getBytes(StandardCharsets.UTF_8), outofbounds); } - private void runWith(String... arguments) throws IOException { + private static Stream configurationCacheProvider() { + return Stream.of(false, true); + } + + private void runWith(boolean configurationCache, String... arguments) throws IOException { StringBuilder output = new StringBuilder(); StringBuilder error = new StringBuilder(); try (Writer outputWriter = new StringPrinter(output::append).toWriter(); Writer errorWriter = new StringPrinter(error::append).toWriter();) { - gradleRunner() + gradleRunner(configurationCache) .withArguments(arguments) .forwardStdOutput(outputWriter) .forwardStdError(errorWriter) @@ -72,37 +81,60 @@ private void runWith(String... arguments) throws IOException { this.error = error.toString(); } - @Test - void dirty() throws IOException { - runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + dirty.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + protected GradleRunner gradleRunner(boolean configurationCache) throws IOException { + if (configurationCache) { + setFile("gradle.properties").toContent("org.gradle.unsafe.configuration-cache=true"); + setFile("settings.gradle").toContent("enableFeaturePreview(\"STABLE_CONFIGURATION_CACHE\")"); + return super.gradleRunner().withGradleVersion(GradleVersionSupport.STABLE_CONFIGURATION_CACHE.version); + } else { + File gradleProps = new File(rootFolder(), "gradle.properties"); + if (gradleProps.exists()) { + gradleProps.delete(); + } + File settingsGradle = new File(rootFolder(), "settings.gradle"); + if (settingsGradle.exists()) { + settingsGradle.delete(); + } + return super.gradleRunner(); + } + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void dirty(boolean configurationCache) throws IOException { + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + dirty.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEqualTo("abc"); Assertions.assertThat(error).startsWith("IS DIRTY"); } - @Test - void clean() throws IOException { - runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + clean.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void clean(boolean configurationCache) throws IOException { + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + clean.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEmpty(); Assertions.assertThat(error).startsWith("IS CLEAN"); } - @Test - void diverge() throws IOException { - runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + diverge.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void diverge(boolean configurationCache) throws IOException { + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + diverge.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEmpty(); Assertions.assertThat(error).startsWith("DID NOT CONVERGE"); } - @Test - void outofbounds() throws IOException { - runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + outofbounds.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void outofbounds(boolean configurationCache) throws IOException { + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + outofbounds.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEmpty(); Assertions.assertThat(error).isEmpty(); } - @Test - void notAbsolute() throws IOException { - runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut"); + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void notAbsolute(boolean configurationCache) throws IOException { + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEmpty(); Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path"); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndentIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndentIntegrationTest.java new file mode 100644 index 0000000000..fc883b41e9 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndentIntegrationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021-2024 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 java.util.stream.Stream; + +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class IndentIntegrationTest extends GradleIntegrationHarness { + + @ParameterizedTest + @ValueSource(strings = {"indentWithSpaces", "indentWithTabs"}) + void oldIndentApiLogsDeprecationWarning(String indentationMethodName) throws IOException { + BuildResult result = runIndentFormatter(indentationMethodName); + assertThat(result.getOutput()).containsPattern(".*" + indentationMethodName + ".*deprecated.*"); + } + + @ParameterizedTest + @ValueSource(strings = {"leadingTabsToSpaces", "leadingSpacesToTabs"}) + void newIndentApiDoesNotLogDeprecationWarning(String indentationMethodName) throws IOException { + BuildResult result = runIndentFormatter(indentationMethodName); + assertThat(result.getOutput()).doesNotContainPattern(".*" + indentationMethodName + ".*deprecated.*"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("indentationCombinations") + void indentationCombinations(String testName, String indentationMethodName, String actualResource, String expectedResultResource) throws IOException { + runIndentFormatter(indentationMethodName, actualResource); + assertFile("test.txt").sameAsResource(expectedResultResource); + } + + private static Stream indentationCombinations() { + return Stream.of( + // new API + Arguments.of("tabsToTabs", "leadingSpacesToTabs", "indent/IndentedWithTab.test", "indent/IndentedWithTab.test"), + Arguments.of("spacesToSpaces", "leadingTabsToSpaces", "indent/IndentedWithSpace.test", "indent/IndentedWithSpace.test"), + Arguments.of("spacesToTabs", "leadingSpacesToTabs", "indent/IndentedWithSpace.test", "indent/IndentedWithTab.test"), + Arguments.of("tabsToSpaces", "leadingTabsToSpaces", "indent/IndentedWithTab.test", "indent/IndentedWithSpace.test"), + Arguments.of("mixedToTabs", "leadingSpacesToTabs", "indent/IndentedMixed.test", "indent/IndentedWithTab.test"), + Arguments.of("mixedToSpaces", "leadingTabsToSpaces", "indent/IndentedMixed.test", "indent/IndentedWithSpace.test"), + // legacy API + Arguments.of("legacy: tabsToTabs", "indentWithTabs", "indent/IndentedWithTab.test", "indent/IndentedWithTab.test"), + Arguments.of("legacy: spacesToSpaces", "indentWithSpaces", "indent/IndentedWithSpace.test", "indent/IndentedWithSpace.test"), + Arguments.of("legacy: spacesToTabs", "indentWithTabs", "indent/IndentedWithSpace.test", "indent/IndentedWithTab.test"), + Arguments.of("legacy: tabsToSpaces", "indentWithSpaces", "indent/IndentedWithTab.test", "indent/IndentedWithSpace.test"), + Arguments.of("legacy: mixedToTabs", "indentWithTabs", "indent/IndentedMixed.test", "indent/IndentedWithTab.test"), + Arguments.of("legacy: mixedToSpaces", "indentWithSpaces", "indent/IndentedMixed.test", "indent/IndentedWithSpace.test")); + } + + private BuildResult runIndentFormatter(String indentationMethodName) throws IOException { + return runIndentFormatter(indentationMethodName, "indent/IndentedMixed.test"); + } + + private BuildResult runIndentFormatter(String indentationMethodName, String resourceFile) throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " format 'test', {", + " target '**/*.txt'", + " " + indentationMethodName + "()", + " }", + "}"); + setFile("test.txt").toResource(resourceFile); + BuildResult result = gradleRunner().withArguments("spotlessApply").build(); + return result; + } + +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndependentTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndependentTaskTest.java index 459b871b94..4062aae51d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndependentTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IndependentTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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 @@ void independent() throws IOException { "", "def underTest = new JavaExtension(spotless)", "underTest.target file('test.java')", - "underTest.googleJavaFormat('1.2')", + "underTest.googleJavaFormat()", "", "def independent = underTest.createIndependentApplyTask('independent')"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); 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 898008c27a..eed4cf1a4d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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 @@ void integration() throws IOException { "", "spotless {", " java {", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); setFile("src/main/java/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -62,4 +62,22 @@ void multipleBlocksShouldWork() throws IOException { gradleRunner().withArguments("spotlessApply").build(); gradleRunner().withArguments("spotlessApply").build(); } + + @Test + void removeUnusedImportsWithCleanthat() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'java'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java { removeUnusedImports('cleanthat-javaparser-unnecessaryimport') }", + "}"); + + setFile("src/main/java/test.java").toResource("java/removeunusedimports/Jdk17TextBlockUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/java/test.java").sameAsResource("java/removeunusedimports/Jdk17TextBlockFormatted.test"); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaEclipseTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaEclipseTest.java new file mode 100644 index 0000000000..c102b7abe1 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavaEclipseTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2024 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 JavaEclipseTest extends GradleIntegrationHarness { + @Test + void settingsWithContentWithoutFile() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'java'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java { eclipse().configProperties(\"\"\"", + "valid_line_oriented.prefs.string=string", + "\"\"\") }", + "}"); + + gradleRunner().withArguments("spotlessApply").build(); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavascriptExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavascriptExtensionTest.java new file mode 100644 index 0000000000..f1b00706d3 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JavascriptExtensionTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2016-2023 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.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintStyleGuide; +import com.diffplug.spotless.tag.NpmTest; + +@NpmTest +class JavascriptExtensionTest extends GradleIntegrationHarness { + + private static String styleGuideMapString(String styleGuideName) { + return EslintStyleGuide.fromNameOrNull(styleGuideName).asGradleMapStringMergedWith(EslintFormatterStep.defaultDevDependencies()); + } + + @NpmTest + @Nested + class EslintGeneralJavascriptTests extends GradleIntegrationHarness { + @Test + void supportsEslintFormattingForJavascript() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/javascript/styleguide/standard/.eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint(" + styleGuideMapString("standard") + ").configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.js").toResource("npm/eslint/javascript/styleguide/standard/javascript-es6.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource("npm/eslint/javascript/styleguide/standard/javascript-es6.clean"); + } + + @Test + void eslintAllowsToSpecifyEslintVersionForJavascript() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/javascript/custom_rules/.eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint('8.28.0').configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.js").toResource("npm/eslint/javascript/custom_rules/javascript-es6.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource("npm/eslint/javascript/custom_rules/javascript-es6.clean"); + } + + @Test + void esllintAllowsToSpecifyInlineConfig() throws IOException { + final String eslintConfigJs = String.join("\n", + "{", + " env: {", + " browser: true,", + " es2021: true", + " },", + " extends: 'standard',", + " overrides: [", + " ],", + " parserOptions: {", + " ecmaVersion: 'latest',", + " sourceType: 'module'", + " },", + " rules: {", + " }", + "}"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint(" + styleGuideMapString("standard") + ").configJs('''" + eslintConfigJs + "''')", + " }", + "}"); + setFile("test.js").toResource("npm/eslint/javascript/styleguide/standard/javascript-es6.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource("npm/eslint/javascript/styleguide/standard/javascript-es6.clean"); + } + + @Test + void eslintRequiresAnExplicitEslintConfig() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/javascript/styleguide/standard/.eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint(" + styleGuideMapString("standard") + ")", + " }", + "}"); + setFile("test.js").toResource("npm/eslint/javascript/styleguide/standard/javascript-es6.dirty"); + BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail(); + Assertions.assertThat(spotlessApply.getOutput()).contains("ESLint must be configured"); + } + + @Test + void eslintAllowsSpecifyingCustomLibraryVersions() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/javascript/styleguide/standard/.eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint([", + " 'eslint': '8.28.0',", + " 'eslint-config-standard': '17.0.0',", + " 'eslint-plugin-import': '2.26.0',", + " 'eslint-plugin-n': '15.6.0',", + " 'eslint-plugin-promise': '6.1.1'", + " ]).configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.js").toResource("npm/eslint/javascript/styleguide/standard/javascript-es6.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource("npm/eslint/javascript/styleguide/standard/javascript-es6.clean"); + } + + } + + @NpmTest + @Nested + class EslintPopularJsStyleGuideTests extends GradleIntegrationHarness { + @ParameterizedTest(name = "{index}: eslint can be applied using styleguide {0}") + @ValueSource(strings = {"airbnb", "google", "standard", "xo"}) + void formattingUsingStyleguide(String styleguide) throws Exception { + + final String styleguidePath = "npm/eslint/javascript/styleguide/" + styleguide + "/"; + + setFile(".eslintrc.js").toResource(styleguidePath + ".eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " eslint(" + styleGuideMapString(styleguide) + ").configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.js").toResource(styleguidePath + "javascript-es6.dirty"); + gradleRunner().forwardOutput().withArguments("--info", "--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource(styleguidePath + "javascript-es6.clean"); + } + } + + @NpmTest + @Nested + class JavascriptPrettierTests extends GradleIntegrationHarness { + @Test + void supportsPrettierFormattingForJavascript() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target 'test.js'", + " prettier()", + " }", + "}"); + setFile("test.js").toResource("npm/prettier/filetypes/javascript-es6/javascript-es6.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.js").sameAsResource("npm/prettier/filetypes/javascript-es6/javascript-es6.clean"); + } + } + +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java index edcd4b428f..73e443b915 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ class JsonExtensionTest extends GradleIntegrationHarness { @Test - void defaultFormatting() throws IOException { + void simpleDefaultFormatting() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'java'", @@ -30,9 +30,9 @@ void defaultFormatting() throws IOException { "repositories { mavenCentral() }", "spotless {", " json {", - " target 'examples/**/*.json'", - " simple()", - "}", + " target 'examples/**/*.json'", + " simple()", + " }", "}"); setFile("src/main/resources/example.json").toResource("json/nestedObjectBefore.json"); setFile("examples/main/resources/example.json").toResource("json/nestedObjectBefore.json"); @@ -42,7 +42,7 @@ void defaultFormatting() throws IOException { } @Test - void formattingWithCustomNumberOfSpaces() throws IOException { + void simpleFormattingWithCustomNumberOfSpaces() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'java'", @@ -51,12 +51,149 @@ void formattingWithCustomNumberOfSpaces() throws IOException { "repositories { mavenCentral() }", "spotless {", " json {", - " target 'src/**/*.json'", - " simple().indentWithSpaces(6)", + " target 'src/**/*.json'", + " simple().indentWithSpaces(6)", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/singletonArrayBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/singletonArrayAfter6Spaces.json"); + } + + @Test + void gsonDefaultFormatting() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'examples/**/*.json'", + " gson()", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/nestedObjectBefore.json"); + setFile("examples/main/resources/example.json").toResource("json/nestedObjectBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/nestedObjectBefore.json"); + assertFile("examples/main/resources/example.json").sameAsResource("json/nestedObjectAfter.json"); + } + + @Test + void gsonFormattingWithCustomNumberOfSpaces() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " gson().indentWithSpaces(6)", + " }", "}"); setFile("src/main/resources/example.json").toResource("json/singletonArrayBefore.json"); gradleRunner().withArguments("spotlessApply").build(); assertFile("src/main/resources/example.json").sameAsResource("json/singletonArrayAfter6Spaces.json"); } + + @Test + void gsonFormattingWithSortingByKeys() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " gson().sortByKeys()", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/sortByKeysBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/sortByKeysAfter.json"); + } + + @Test + void gsonFormattingWithHtmlEscape() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " gson().escapeHtml()", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/escapeHtmlGsonBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/escapeHtmlGsonAfter.json"); + } + + @Test + void jacksonFormattingWithSortingByKeys() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " jackson().feature('ORDER_MAP_ENTRIES_BY_KEYS', true)", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/sortByKeysBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/sortByKeysAfter_Jackson.json"); + } + + @Test + void jsonPatchReplaceString() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " jsonPatch([[op: 'replace', path: '/abc', value: 'ghi']])", + " gson()", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/patchObjectBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/patchObjectAfterReplaceString.json"); + } + + @Test + void jsonPatchReplaceWithObject() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target 'src/**/*.json'", + " jsonPatch([[op: 'replace', path: '/abc', value: [def: 'ghi']]])", + " gson()", + " }", + "}"); + setFile("src/main/resources/example.json").toResource("json/patchObjectBefore.json"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.json").sameAsResource("json/patchObjectAfterReplaceWithObject.json"); + } } 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 f3d8ad442d..a32ef06acd 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,21 @@ package com.diffplug.gradle.spotless; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; +import java.io.File; import java.io.IOException; -import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; class KotlinExtensionTest extends GradleIntegrationHarness { private static final String HEADER = "// License Header"; private static final String HEADER_WITH_YEAR = "// License Header $YEAR"; - @Test - void integration() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlin {", - " ktlint()", - " }", - "}"); - setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktlint/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktlint/basic.clean"); - } - @Test void integrationDiktat() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.4.30'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", @@ -65,208 +45,195 @@ void integrationDiktat() throws IOException { } @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void integrationKtfmt() throws IOException { - setFile("build.gradle").toLines( + void integrationKtfmtDropboxStyleWithPublicApi() throws IOException { + setFile("build.gradle.kts").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", + " id(\"org.jetbrains.kotlin.jvm\") version \"1.6.21\"", + " id(\"com.diffplug.spotless\")", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " ktfmt()", + " ktfmt(\"0.50\").dropboxStyle().configure {", + " it.setMaxWidth(4)", + " it.setBlockIndent(4)", + " it.setContinuationIndent(4)", + " it.setRemoveUnusedImports(false)", + " it.setManageTrailingCommas(false)", + " }", " }", "}"); setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktfmt/basic.dirty"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktfmt/basic.clean"); + assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktfmt/basic-dropbox-style.clean"); } @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void integrationKtfmt_dropboxStyle_0_18() throws IOException { + void withExperimentalEditorConfigOverride() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " ktfmt('0.18').dropboxStyle()", + " ktlint().editorConfigOverride([", + " ktlint_experimental: \"enabled\",", + " ij_kotlin_allow_trailing_comma: true,", + " ij_kotlin_allow_trailing_comma_on_call_site: true", + " ])", " }", "}"); - setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktfmt/basic.dirty"); + setFile("src/main/kotlin/Main.kt").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktfmt/basic-dropboxstyle.clean"); + assertFile("src/main/kotlin/Main.kt").sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.clean"); } @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void integrationKtfmt_dropboxStyle_0_19() throws IOException { + void testWithInvalidEditorConfigFile() throws IOException { + String invalidPath = "invalid/path/to/.editorconfig".replace('/', File.separatorChar); + setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " ktfmt('0.19').dropboxStyle()", + " ktlint().setEditorConfigPath('" + invalidPath.replace("\\", "\\\\") + "')", " }", "}"); - setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktfmt/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktfmt/basic-dropboxstyle.clean"); + setFile("src/main/kotlin/Main.kt").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); + String buildOutput = gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput(); + assertThat(buildOutput).contains("EditorConfig file does not exist: "); + assertThat(buildOutput).contains(invalidPath); } @Test - void testWithIndentation() throws IOException { + void testReadCodeStyleFromEditorConfigFile() throws IOException { + setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " ktlint('0.32.0').userData(['indent_size': '6'])", + " ktlint()", " }", "}"); - setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktlint/basic.dirty"); - BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); - assertThat(result.getOutput()).contains("Unexpected indentation (4) (it should be 6)"); + checkKtlintOfficialStyle(); } @Test - void testWithHeader() throws IOException { + void testEditorConfigOverrideWithUnsetCodeStyleDoesNotOverrideEditorConfigCodeStyleWithDefault() throws IOException { + setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " licenseHeader('" + HEADER + "')", - " ktlint()", + " ktlint().editorConfigOverride([", + " ktlint_test_key: true,", + " ])", " }", "}"); - setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test")); + checkKtlintOfficialStyle(); } @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void testWithHeaderKtfmt() throws IOException { + void testSetEditorConfigCanOverrideEditorConfigFile() throws IOException { + setFile(".editorconfig").toResource("kotlin/ktlint/intellij_idea/.editorconfig"); setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " licenseHeader('" + HEADER + "')", - " ktfmt()", + " ktlint().editorConfigOverride([", + " ktlint_code_style: \"ktlint_official\",", + " ])", " }", "}"); - setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeaderKtfmt.test")); + checkKtlintOfficialStyle(); } @Test - void testWithCustomHeaderSeparator() throws IOException { - setFile("build.gradle").toLines( + void withCustomRuleSetApply() throws IOException { + setFile("build.gradle.kts").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", + " id(\"org.jetbrains.kotlin.jvm\") version \"1.6.21\"", + " id(\"com.diffplug.spotless\")", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " licenseHeader ('" + HEADER + "', '@file')", - " ktlint()", + " ktlint(\"1.0.1\")", + " .customRuleSets(listOf(", + " \"io.nlopez.compose.rules:ktlint:0.4.16\"", + " ))", + " .editorConfigOverride(mapOf(", + " \"ktlint_function_naming_ignore_when_annotated_with\" to \"Composable\"", + " ))", " }", "}"); - setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test")); + setFile("src/main/kotlin/Main.kt").toResource("kotlin/ktlint/listScreen.dirty"); + String buildOutput = gradleRunner().withArguments("spotlessCheck").buildAndFail().getOutput(); + assertThat(buildOutput).contains("Composable functions that return Unit should start with an uppercase letter."); } @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void testWithCustomHeaderSeparatorKtfmt() throws IOException { + void testWithHeader() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " licenseHeader ('" + HEADER + "', '@file')", - " ktfmt()", + " ktlint()", + " licenseHeader('" + HEADER + "')", " }", "}"); setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeaderKtfmt.test")); + assertFile("src/main/kotlin/AnObject.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test")); } @Test - void testWithNonStandardYearSeparator() throws IOException { + void testWithCustomMaxWidthDefaultStyleKtfmt() throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlin {", - " licenseHeader('" + HEADER_WITH_YEAR + "').yearSeparator(', ')", - " ktlint()", + " ktfmt().configure { options ->", + " options.maxWidth = 120", + " }", " }", "}"); - setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test"); - setFile("src/main/kotlin/AnObject2.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test"); + setFile("src/main/kotlin/max-width.kt").toResource("kotlin/ktfmt/max-width.dirty"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").matches(matcher -> { - matcher.startsWith("// License Header 2012, 2014"); - }); - assertFile("src/main/kotlin/AnObject2.kt").matches(matcher -> { - matcher.startsWith("// License Header 2012, 2014"); - }); + assertFile("src/main/kotlin/max-width.kt").sameAsResource("kotlin/ktfmt/max-width.clean"); } - @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void testWithNonStandardYearSeparatorKtfmt() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlin {", - " licenseHeader('" + HEADER_WITH_YEAR + "').yearSeparator(', ')", - " ktfmt()", - " }", - "}"); - - setFile("src/main/kotlin/AnObject.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test"); - setFile("src/main/kotlin/AnObject2.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test"); + private void checkKtlintOfficialStyle() throws IOException { + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("src/main/kotlin/AnObject.kt").matches(matcher -> { - matcher.startsWith("// License Header 2012, 2014"); - }); - assertFile("src/main/kotlin/AnObject2.kt").matches(matcher -> { - matcher.startsWith("// License Header 2012, 2014"); - }); + assertFile(path).sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.ktlintOfficial.clean"); } } 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 c68de0d7e7..c607c36912 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-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,172 +15,44 @@ */ package com.diffplug.gradle.spotless; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; - import java.io.IOException; -import org.gradle.testkit.runner.BuildResult; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; - -class KotlinGradleExtensionTest extends GradleIntegrationHarness { - @Test - void integration() throws IOException { - testInDirectory(null); - } - - @Test - void integration_script_in_subdir() throws IOException { - testInDirectory("companionScripts"); - } +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; - private void testInDirectory(final String directory) throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktlint()", - " target '**/*.gradle.kts'", - " }", - "}"); - String filePath = "configuration.gradle.kts"; - if (directory != null) { - filePath = directory + "/" + filePath; - } - setFile(filePath).toResource("kotlin/ktlint/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile(filePath).sameAsResource("kotlin/ktlint/basic.clean"); - } - - @Test - void integration_default() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktlint()", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/ktlint/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/basic.clean"); - } - - @Test - void integration_default_diktat() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.4.30'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " diktat()", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/diktat/basic.dirty"); - BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); - assertThat(result.getOutput()).contains("[AVOID_NESTED_FUNCTIONS] try to avoid using nested functions"); - } - - @Test - void integration_pinterest() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktlint('0.32.0')", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/ktlint/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/basic.clean"); - } - - @Test - void indentStep() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktlint().userData(['indent_size': '6'])", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/ktlint/basic.dirty"); - gradleRunner().withArguments("spotlessCheck").buildAndFail(); - } - - @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void integration_ktfmt() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktfmt()", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/ktfmt/basic.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktfmt/basic.clean"); - } - - @Test - @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. - void integration_ktfmt_with_dropbox_style() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless {", - " kotlinGradle {", - " ktfmt().dropboxStyle()", - " }", - "}"); - setFile("configuration.gradle.kts").toResource("kotlin/ktfmt/dropboxstyle.dirty"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktfmt/dropboxstyle.clean"); - } +/** + * We test KotlinGradleExtension only behaviors here. + */ +class KotlinGradleExtensionTest extends KotlinExtensionTest { - @Test - void integration_lint_script_files_without_top_level_declaration() throws IOException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testTarget(boolean useDefaultTarget) throws IOException { setFile("build.gradle").toLines( "plugins {", - " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'org.jetbrains.kotlin.jvm' version '1.6.21'", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless {", " kotlinGradle {", - " ktlint()", + " " + (useDefaultTarget ? "" : "target \"*.kts\""), + " ktlint().editorConfigOverride([", + " ktlint_experimental: \"enabled\",", + " ij_kotlin_allow_trailing_comma: true,", + " ij_kotlin_allow_trailing_comma_on_call_site: true", + " ])", " }", "}"); - setFile("configuration.gradle.kts").toContent("buildscript {}"); + setFile("configuration.gradle.kts").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); + setFile("configuration.kts").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); gradleRunner().withArguments("spotlessApply").build(); - assertFile("configuration.gradle.kts").hasContent("buildscript {}"); + if (useDefaultTarget) { + assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.clean"); + assertFile("configuration.kts").sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); + } else { + assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.clean"); + assertFile("configuration.kts").sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.clean"); + } } } 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 4b0784832f..d4c01cdc6d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ import org.eclipse.jgit.api.Git; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ClearGitConfig; + +@ClearGitConfig class LicenseHeaderTest extends GradleIntegrationHarness { private static final String NOW = String.valueOf(YearMonth.now().getYear()); 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 086a2ddb1d..5d184e15e5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ void createSubproject(String name) throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.2')", + " googleJavaFormat('1.17.0')", " }", "}"); setFile(name + "/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -71,7 +71,7 @@ public void hasRootSpotless() throws IOException { "spotless {", " java {", " target file('test.java')", - " googleJavaFormat('1.2')", + " googleJavaFormat('1.17.0')", " }", "}"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -88,7 +88,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.2] into the `spotlessPredeclare` block in the root project."); + .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.17.0] into the `spotlessPredeclare` block in the root project."); } @Test @@ -100,7 +100,7 @@ public void predeclaredSucceeds() throws IOException { "repositories { mavenCentral() }", "spotless { predeclareDeps() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.2') }", + " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); @@ -115,7 +115,7 @@ public void predeclaredFromBuildscriptSucceeds() throws IOException { "repositories { mavenCentral() }", "spotless { predeclareDepsFromBuildscript() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.2') }", + " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); @@ -129,7 +129,7 @@ public void predeclaredOrdering() throws IOException { "}", "repositories { mavenCentral() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.2') }", + " java { googleJavaFormat('1.17.0') }", "}", "spotless { predeclareDepsFromBuildscript() }"); createNSubprojects(); @@ -145,7 +145,7 @@ public void predeclaredUndeclared() throws IOException { "}", "repositories { mavenCentral() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.2') }", + " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NativeCmdIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NativeCmdIntegrationTest.java index a4f8d6205c..94ecf6c076 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NativeCmdIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NativeCmdIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,26 +20,40 @@ import java.io.File; import java.io.IOException; +import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.Test; -class NativeCmdIntegrationTest extends GradleIntegrationHarness { +interface NativeCmdIntegrationTest { @Test - void nativeCmd() throws IOException { + default void nativeCmd() throws IOException { // This will only work if /usr/bin/sed is available assumeThat(new File("/usr/bin/sed")).exists(); - setFile("build.gradle").toLines( + GradleIntegrationHarness harness = (GradleIntegrationHarness) this; + harness.setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", "}", "spotless {", + " lineEndings 'UNIX'", " format 'test', {", - " target '**/*.txt'", + " target '*.txt'", " nativeCmd('sed', '/usr/bin/sed', ['s/placeholder/replaced/g'])", " }", "}"); - setFile("test.txt").toResource("native_cmd/dirty.txt"); - gradleRunner().withArguments("spotlessApply").build(); - assertFile("test.txt").sameAsResource("native_cmd/clean.txt"); + harness.setFile("test.txt").toResource("native_cmd/dirty.txt"); + harness.gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + harness.assertFile("test.txt").sameAsResource("native_cmd/clean.txt"); + } + + class NativeCmdWithoutConfigCacheTest extends GradleIntegrationHarness implements NativeCmdIntegrationTest {} + + class NativeCmdWithConfigCacheTest extends GradleIntegrationHarness implements NativeCmdIntegrationTest { + @Override + public GradleRunner gradleRunner() throws IOException { + setFile("gradle.properties").toLines("org.gradle.unsafe.configuration-cache=true", + "org.gradle.configuration-cache=true"); + return super.gradleRunner().withGradleVersion(GradleVersionSupport.CONFIGURATION_CACHE.version); + } } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmInstallCacheIntegrationTests.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmInstallCacheIntegrationTests.java new file mode 100644 index 0000000000..e7ad5f1654 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmInstallCacheIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2023 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 com.diffplug.gradle.spotless.FormatExtension.NpmStepConfig.SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.io.TempDir; + +import com.diffplug.common.base.Errors; +import com.diffplug.spotless.tag.NpmTest; + +@TestMethodOrder(OrderAnnotation.class) +@NpmTest +class NpmInstallCacheIntegrationTests extends GradleIntegrationHarness { + + static File pertainingCacheDir; + + private static final File DEFAULT_DIR_FOR_NPM_INSTALL_CACHE_DO_NEVER_WRITE_TO_THIS = new File("."); + + @BeforeAll + static void beforeAll(@TempDir File pertainingCacheDir) { + NpmInstallCacheIntegrationTests.pertainingCacheDir = Errors.rethrow().get(pertainingCacheDir::getCanonicalFile); + } + + @Test + void prettierCachesNodeModulesToADefaultFolderWhenCachingEnabled() throws IOException { + File dir1 = newFolder("npm-prettier-1"); + File cacheDir = DEFAULT_DIR_FOR_NPM_INSTALL_CACHE_DO_NEVER_WRITE_TO_THIS; + BuildResult result = runPhpPrettierOnDir(dir1, cacheDir); + Assertions.assertThat(result.getOutput()) + .doesNotContain("Using cached node_modules for") + .contains("Caching node_modules for ") + .contains(Paths.get(dir1.getAbsolutePath(), "build", SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME).toString()); + + } + + @Test + void prettierCachesAndReusesNodeModulesInSpecificInstallCacheFolder() throws IOException { + File dir1 = newFolder("npm-prettier-1"); + File cacheDir = newFolder("npm-prettier-cache"); + BuildResult result = runPhpPrettierOnDir(dir1, cacheDir); + Assertions.assertThat(result.getOutput()).doesNotContainPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + File dir2 = newFolder("npm-prettier-2"); + BuildResult result2 = runPhpPrettierOnDir(dir2, cacheDir); + Assertions.assertThat(result2.getOutput()).containsPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + void prettierDoesNotCacheNodeModulesIfNotExplicitlyEnabled() throws IOException { + File dir2 = newFolder("npm-prettier-1"); + BuildResult result = runPhpPrettierOnDir(dir2, null); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*") + .doesNotContainPattern("Caching node_modules for .*"); + } + + @Test + @Order(1) + void prettierCachesNodeModuleInGlobalInstallCacheDir() throws IOException { + File dir1 = newFolder("npm-prettier-global-1"); + File cacheDir = pertainingCacheDir; + BuildResult result = runPhpPrettierOnDir(dir1, cacheDir); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .containsPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + @Order(2) + void prettierUsesCachedNodeModulesFromGlobalInstallCacheDir() throws IOException { + File dir2 = newFolder("npm-prettier-global-2"); + File cacheDir = pertainingCacheDir; + BuildResult result = runPhpPrettierOnDir(dir2, cacheDir); + Assertions.assertThat(result.getOutput()) + .containsPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .doesNotContainPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + private BuildResult runPhpPrettierOnDir(File projDir, File cacheDir) throws IOException { + String baseDir = projDir.getName(); + String cacheDirEnabled = cacheDirEnabledStringForCacheDir(cacheDir); + setFile(baseDir + "/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierConfig = [:]", + "prettierConfig['tabWidth'] = 3", + "prettierConfig['parser'] = 'php'", + "def prettierPackages = [:]", + "prettierPackages['prettier'] = '2.8.8'", + "prettierPackages['@prettier/plugin-php'] = '0.19.6'", + "spotless {", + " format 'php', {", + " target 'php-example.php'", + " prettier(prettierPackages).config(prettierConfig)" + cacheDirEnabled, + " }", + "}"); + setFile(baseDir + "/php-example.php").toResource("npm/prettier/plugins/php.dirty"); + final BuildResult spotlessApply = gradleRunner().withProjectDir(projDir).withArguments("--stacktrace", "--info", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile(baseDir + "/php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); + return spotlessApply; + } + + @Test + @Order(3) + void tsfmtCachesNodeModuleInGlobalInstallCacheDir() throws IOException { + File dir1 = newFolder("npm-tsfmt-global-1"); + File cacheDir = pertainingCacheDir; + BuildResult result = runTsfmtOnDir(dir1, cacheDir); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .containsPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + @Order(4) + void tsfmtUsesCachedNodeModulesFromGlobalInstallCacheDir() throws IOException { + File dir2 = newFolder("npm-tsfmt-global-2"); + File cacheDir = pertainingCacheDir; + BuildResult result = runTsfmtOnDir(dir2, cacheDir); + Assertions.assertThat(result.getOutput()) + .containsPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .doesNotContainPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + void tsfmtDoesNotCacheNodeModulesIfNotExplicitlyEnabled() throws IOException { + File dir2 = newFolder("npm-tsfmt-1"); + BuildResult result = runTsfmtOnDir(dir2, null); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*") + .doesNotContainPattern("Caching node_modules for .*"); + } + + private BuildResult runTsfmtOnDir(File projDir, File cacheDir) throws IOException { + String baseDir = projDir.getName(); + String cacheDirEnabled = cacheDirEnabledStringForCacheDir(cacheDir); + setFile(baseDir + "/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def tsfmtconfig = [:]", + "tsfmtconfig['indentSize'] = 1", + "tsfmtconfig['convertTabsToSpaces'] = true", + "spotless {", + " typescript {", + " target 'test.ts'", + " tsfmt().config(tsfmtconfig)" + cacheDirEnabled, + " }", + "}"); + setFile(baseDir + "/test.ts").toResource("npm/tsfmt/tsfmt/tsfmt.dirty"); + final BuildResult spotlessApply = gradleRunner().withProjectDir(projDir).withArguments("--stacktrace", "--info", "spotlessApply").build(); + assertFile(baseDir + "/test.ts").sameAsResource("npm/tsfmt/tsfmt/tsfmt.clean"); + return spotlessApply; + } + + @Test + @Order(5) + void eslintCachesNodeModuleInGlobalInstallCacheDir() throws IOException { + File dir1 = newFolder("npm-eslint-global-1"); + File cacheDir = pertainingCacheDir; + BuildResult result = runEslintOnDir(dir1, cacheDir); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .containsPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + @Order(6) + void eslintUsesCachedNodeModulesFromGlobalInstallCacheDir() throws IOException { + File dir2 = newFolder("npm-eslint-global-2"); + File cacheDir = pertainingCacheDir; + BuildResult result = runEslintOnDir(dir2, cacheDir); + Assertions.assertThat(result.getOutput()) + .containsPattern("Using cached node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E") + .doesNotContainPattern("Caching node_modules for .*\\Q" + cacheDir.getAbsolutePath() + "\\E"); + } + + @Test + void eslintDoesNotCacheNodeModulesIfNotExplicitlyEnabled() throws IOException { + File dir2 = newFolder("npm-eslint-1"); + File cacheDir = null; + BuildResult result = runEslintOnDir(dir2, cacheDir); + Assertions.assertThat(result.getOutput()) + .doesNotContainPattern("Using cached node_modules for .*") + .doesNotContainPattern("Caching node_modules for .*"); + } + + private BuildResult runEslintOnDir(File projDir, File cacheDir) throws IOException { + String baseDir = projDir.getName(); + String cacheDirEnabled = cacheDirEnabledStringForCacheDir(cacheDir); + + setFile(baseDir + "/.eslintrc.js").toResource("npm/eslint/typescript/custom_rules/.eslintrc.js"); + setFile(baseDir + "/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " eslint().configFile('.eslintrc.js')" + cacheDirEnabled, + " }", + "}"); + setFile(baseDir + "/test.ts").toResource("npm/eslint/typescript/custom_rules/typescript.dirty"); + BuildResult spotlessApply = gradleRunner().withProjectDir(projDir).withArguments("--stacktrace", "--info", "spotlessApply").build(); + assertFile(baseDir + "/test.ts").sameAsResource("npm/eslint/typescript/custom_rules/typescript.clean"); + return spotlessApply; + } + + private static String cacheDirEnabledStringForCacheDir(File cacheDir) { + String cacheDirEnabled; + if (cacheDir == null) { + cacheDirEnabled = ""; + } else if (cacheDir == DEFAULT_DIR_FOR_NPM_INSTALL_CACHE_DO_NEVER_WRITE_TO_THIS) { + cacheDirEnabled = ".npmInstallCache()"; + } else { + cacheDirEnabled = ".npmInstallCache('" + cacheDir.getAbsolutePath() + "')"; + } + return cacheDirEnabled; + } +} 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 new file mode 100644 index 0000000000..6abbc108fa --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2016-2025 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 org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.BuildResult; +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.Predicates; + +class NpmTestsWithoutNpmInstallationTest extends GradleIntegrationHarness { + + @Test + void useNodeAndNpmFromNodeGradlePlugin() throws Exception { + try { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'com.github.node-gradle.node' version '3.5.1'", + "}", + "repositories { mavenCentral() }", + "node {", + " download = true", + " version = '18.16.1'", + " npmVersion = '9.5.1'", + " workDir = file(\"${buildDir}/nodejs\")", + " npmWorkDir = file(\"${buildDir}/npm\")", + "}", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 20", + "prettierConfig['parser'] = 'typescript'", + "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'", + "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier()", + " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")", + " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")", + " .config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + // make sure node binary is there + gradleRunner().withArguments("nodeSetup", "npmSetup").build(); + // then run spotless using that node installation + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + private void printContents() { + System.out.println("************* Folder contents **************************"); + System.out.println(listFiles(Predicates.and(path -> !path.startsWith(".gradle"), path -> !path.contains("/node_modules/"), path -> !path.contains("/include/")))); + System.out.println("********************************************************"); + } + + @Test + void useNodeAndNpmFromNodeGradlePlugin_example1() throws Exception { + try { + setFile("build.gradle").toResource("com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + @Test + void useNpmFromNodeGradlePlugin_example2() throws Exception { + try { + setFile("build.gradle").toResource("com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + @Test + void useNpmFromNodeGradlePlugin() throws Exception { + try { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'com.github.node-gradle.node' version '3.5.1'", + "}", + "repositories { mavenCentral() }", + "node {", + " download = true", + " version = '18.16.1'", + " workDir = file(\"${buildDir}/nodejs\")", + "}", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 20", + "prettierConfig['parser'] = 'typescript'", + "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier()", + " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")", + " .config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + // make sure node binary is there + gradleRunner().withArguments("nodeSetup", "npmSetup").build(); + // then run spotless using that node installation + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + @Test + void useNpmNextToConfiguredNodePluginFromNodeGradlePlugin() throws Exception { + try { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + " id 'com.github.node-gradle.node' version '3.5.1'", + "}", + "repositories { mavenCentral() }", + "node {", + " download = true", + " version = '18.13.0'", + " workDir = file(\"${buildDir}/nodejs\")", + "}", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 20", + "prettierConfig['parser'] = 'typescript'", + "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier()", + " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")", + " .config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + // make sure node binary is there + gradleRunner().withArguments("nodeSetup", "npmSetup").build(); + // then run spotless using that node installation + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } catch (Exception e) { + printContents(); + throw e; + } + } + + @Test + public void supportsConfigurationCache() throws Exception { + setFile("build.gradle").toResource("com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + BuildResult spotlessApply = gradleRunner() + .withGradleVersion(GradleVersionSupport.STABLE_CONFIGURATION_CACHE.version) + .withArguments("--stacktrace", "--configuration-cache", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java index bc7a2fdc96..888f11f294 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PaddedCellTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.gradle.api.Project; import org.gradle.api.provider.Provider; @@ -30,10 +31,11 @@ import com.diffplug.common.base.StringPrinter; import com.diffplug.spotless.FileSignature; -import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.NeverUpToDateStep; import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.SerializedFunction; import com.diffplug.spotless.TestProvisioner; class PaddedCellTaskTest extends ResourceHarness { @@ -52,21 +54,21 @@ public BuildServiceParameters.None getParameters() { SpotlessCheck check; SpotlessApply apply; - Bundle(String name, FormatterFunc function) throws IOException { + Bundle(String name, SerializedFunction function) throws IOException { this.name = name; file = setFile("src/test." + name).toContent("CCC"); - FormatterStep step = FormatterStep.createNeverUpToDate(name, function); + FormatterStep step = NeverUpToDateStep.create(name, function); source = createFormatTask(name, step); check = createCheckTask(name, source); apply = createApplyTask(name, source); - outputFile = new File(source.getOutputDirectory() + "/src", file.getName()); + outputFile = new File(source.getCleanDirectory() + "/src", file.getName()); } private SpotlessTaskImpl createFormatTask(String name, FormatterStep step) { SpotlessTaskImpl task = project.getTasks().create("spotless" + SpotlessPlugin.capitalize(name), SpotlessTaskImpl.class); task.init(taskService); - task.addStep(step); - task.setLineEndingsPolicy(LineEnding.UNIX.createPolicy()); + task.setSteps(List.of(step)); + task.setLineEndingsPolicy(project.provider(LineEnding.UNIX::createPolicy)); task.setTarget(Collections.singletonList(file)); return task; } 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 2283ce34bf..9990039cd3 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 DiffPlug + * Copyright 2022-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,4 +45,26 @@ void integration() throws IOException { "palantirJavaFormat('1.0.1')"); checkRunsThenUpToDate(); } + + @Test + void formatJavaDoc() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " palantirJavaFormat('2.39.0').formatJavadoc(true)", + " }", + "}"); + + setFile("test.java").toResource("java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.java").sameAsResource("java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test"); + + checkRunsThenUpToDate(); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java index 404ccf88c8..b6547204b8 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,36 +19,52 @@ import org.assertj.core.api.Assertions; import org.gradle.testkit.runner.BuildResult; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import com.diffplug.spotless.npm.PrettierFormatterStep; import com.diffplug.spotless.tag.NpmTest; @NpmTest class PrettierIntegrationTest extends GradleIntegrationHarness { - @Test - void useInlineConfig() throws IOException { + + private static final String PRETTIER_VERSION_2 = PrettierFormatterStep.DEFAULT_VERSION; + + private static final String PRETTIER_VERSION_3 = "3.0.3"; + + @ParameterizedTest(name = "{index}: useInlineConfig with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void useInlineConfig(String prettierVersion) throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "def prettierConfig = [:]", - "prettierConfig['printWidth'] = 50", + "prettierConfig['printWidth'] = 20", "prettierConfig['parser'] = 'typescript'", "spotless {", " format 'mytypescript', {", " target 'test.ts'", - " prettier().config(prettierConfig)", + " prettier('" + prettierVersion + "').config(prettierConfig)", " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); - assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); + switch (prettierVersion) { + case PRETTIER_VERSION_2: + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + break; + case PRETTIER_VERSION_3: + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_3.clean"); + break; + } } - @Test - void verifyCleanSpotlessCheckWorks() throws IOException { + @ParameterizedTest(name = "{index}: verifyCleanSpotlessCheckWorks with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void verifyCleanSpotlessCheckWorks(String prettierVersion) throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -60,19 +76,20 @@ void verifyCleanSpotlessCheckWorks() throws IOException { "spotless {", " format 'mytypescript', {", " target 'test.ts'", - " prettier().config(prettierConfig)", + " prettier('" + prettierVersion + "').config(prettierConfig)", " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); - BuildResult spotlessCheckFailsGracefully = gradleRunner().withArguments("--stacktrace", "spotlessCheck").buildAndFail(); + final BuildResult spotlessCheckFailsGracefully = gradleRunner().withArguments("--stacktrace", "spotlessCheck").buildAndFail(); Assertions.assertThat(spotlessCheckFailsGracefully.getOutput()).contains("> The following files had format violations:"); gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); gradleRunner().withArguments("--stacktrace", "spotlessCheck").build(); } - @Test - void useFileConfig() throws IOException { + @ParameterizedTest(name = "{index}: useFileConfig with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void useFileConfig(String prettierVersion) throws IOException { setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc.yml"); setFile("build.gradle").toLines( "plugins {", @@ -82,17 +99,25 @@ void useFileConfig() throws IOException { "spotless {", " format 'mytypescript', {", " target 'test.ts'", - " prettier().configFile('.prettierrc.yml')", + " prettier('" + prettierVersion + "').configFile('.prettierrc.yml')", " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); - assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); + switch (prettierVersion) { + case PRETTIER_VERSION_2: + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_2.clean"); + break; + case PRETTIER_VERSION_3: + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile_prettier_3.clean"); + break; + } } - @Test - void chooseParserBasedOnFilename() throws IOException { + @ParameterizedTest(name = "{index}: chooseParserBasedOnFilename with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void chooseParserBasedOnFilename(String prettierVersion) throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -101,7 +126,7 @@ void chooseParserBasedOnFilename() throws IOException { "spotless {", " format 'webResources', {", " target 'dirty.*'", - " prettier()", + " prettier('" + prettierVersion + "')", " }", "}"); setFile("dirty.json").toResource("npm/prettier/filename/dirty.json"); @@ -110,8 +135,20 @@ void chooseParserBasedOnFilename() throws IOException { assertFile("dirty.json").sameAsResource("npm/prettier/filename/clean.json"); } - @Test - void useJavaCommunityPlugin() throws IOException { + @ParameterizedTest(name = "{index}: useJavaCommunityPlugin with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void useJavaCommunityPlugin(String prettierVersion) throws IOException { + var prettierPluginJava = ""; + var prettierConfigPluginsStr = ""; + switch (prettierVersion) { + case PRETTIER_VERSION_2: + prettierPluginJava = "2.1.0"; // last version to support v2 + break; + case PRETTIER_VERSION_3: + prettierPluginJava = "2.3.0"; // latest to support v3 + prettierConfigPluginsStr = "prettierConfig['plugins'] = ['prettier-plugin-java']"; + break; + } setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -120,9 +157,10 @@ void useJavaCommunityPlugin() throws IOException { "def prettierConfig = [:]", "prettierConfig['tabWidth'] = 4", "prettierConfig['parser'] = 'java'", + prettierConfigPluginsStr, "def prettierPackages = [:]", - "prettierPackages['prettier'] = '2.0.5'", - "prettierPackages['prettier-plugin-java'] = '0.8.0'", + "prettierPackages['prettier'] = '" + prettierVersion + "'", + "prettierPackages['prettier-plugin-java'] = '" + prettierPluginJava + "'", "spotless {", " format 'java', {", " target 'JavaTest.java'", @@ -135,8 +173,77 @@ void useJavaCommunityPlugin() throws IOException { assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); } - @Test - void usePhpCommunityPlugin() throws IOException { + @ParameterizedTest(name = "{index}: useJavaCommunityPluginFileConfig with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void useJavaCommunityPluginFileConfig(String prettierVersion) throws IOException { + var prettierPluginJava = ""; + switch (prettierVersion) { + case PRETTIER_VERSION_2: + prettierPluginJava = "2.1.0"; // last version to support v2 + break; + case PRETTIER_VERSION_3: + prettierPluginJava = "2.3.0"; // latest to support v3 + break; + } + setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc_java_plugin.yml"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierPackages = [:]", + "prettierPackages['prettier'] = '" + prettierVersion + "'", + "prettierPackages['prettier-plugin-java'] = '" + prettierPluginJava + "'", + "spotless {", + " format 'java', {", + " target 'JavaTest.java'", + " prettier(prettierPackages).configFile('.prettierrc.yml')", + " }", + "}"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); + } + + @ParameterizedTest(name = "{index}: suggestsMissingJavaCommunityPlugin with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void suggestsMissingJavaCommunityPlugin(String prettierVersion) throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierConfig = [:]", + "prettierConfig['tabWidth'] = 4", + "def prettierPackages = [:]", + "prettierPackages['prettier'] = '" + prettierVersion + "'", + "spotless {", + " format 'java', {", + " target 'JavaTest.java'", + " prettier(prettierPackages).config(prettierConfig)", + " }", + "}"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail(); + Assertions.assertThat(spotlessApply.getOutput()).contains("Could not infer a parser"); + Assertions.assertThat(spotlessApply.getOutput()).contains("prettier-plugin-java"); + } + + @ParameterizedTest(name = "{index}: usePhpCommunityPlugin with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void usePhpCommunityPlugin(String prettierVersion) throws IOException { + var prettierPluginPhp = ""; + var prettierConfigPluginsStr = ""; + switch (prettierVersion) { + case PRETTIER_VERSION_2: + prettierPluginPhp = "0.19.7"; // last version to support v2 + break; + case PRETTIER_VERSION_3: + prettierPluginPhp = "0.20.1"; // latest to support v3 + prettierConfigPluginsStr = "prettierConfig['plugins'] = ['@prettier/plugin-php']"; + break; + } setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -145,9 +252,10 @@ void usePhpCommunityPlugin() throws IOException { "def prettierConfig = [:]", "prettierConfig['tabWidth'] = 3", "prettierConfig['parser'] = 'php'", + prettierConfigPluginsStr, "def prettierPackages = [:]", - "prettierPackages['prettier'] = '2.0.5'", - "prettierPackages['@prettier/plugin-php'] = '0.14.2'", + "prettierPackages['prettier'] = '" + prettierVersion + "'", + "prettierPackages['@prettier/plugin-php'] = '" + prettierPluginPhp + "'", "spotless {", " format 'php', {", " target 'php-example.php'", @@ -160,10 +268,77 @@ void usePhpCommunityPlugin() throws IOException { assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); } - @Test - void autodetectNpmrcFileConfig() throws IOException { + /** + * This test is to ensure that we can have multiple prettier instances in one spotless config. + * + * @see Issue #1162 on github + */ + @ParameterizedTest(name = "{index}: usePhpAndJavaCommunityPlugin with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void usePhpAndJavaCommunityPlugin(String prettierVersion) throws IOException { + var prettierPluginJava = ""; + var prettierPluginPhp = ""; + var prettierConfigPluginsJavaStr = ""; + var prettierConfigPluginsPhpStr = ""; + switch (prettierVersion) { + case PRETTIER_VERSION_2: + prettierPluginJava = "2.1.0"; // last version to support v2 + prettierPluginPhp = "0.19.7"; // last version to support v2 + break; + case PRETTIER_VERSION_3: + prettierPluginJava = "2.3.0"; // latest to support v3 + prettierPluginPhp = "0.20.1"; // latest to support v3 + prettierConfigPluginsJavaStr = "prettierConfigJava['plugins'] = ['prettier-plugin-java']"; + prettierConfigPluginsPhpStr = "prettierConfigPhp['plugins'] = ['@prettier/plugin-php']"; + break; + } + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierConfigPhp = [:]", + "prettierConfigPhp['tabWidth'] = 3", + "prettierConfigPhp['parser'] = 'php'", + prettierConfigPluginsPhpStr, + "def prettierPackagesPhp = [:]", + "prettierPackagesPhp['prettier'] = '" + prettierVersion + "'", + "prettierPackagesPhp['@prettier/plugin-php'] = '" + prettierPluginPhp + "'", + "def prettierConfigJava = [:]", + "prettierConfigJava['tabWidth'] = 4", + "prettierConfigJava['parser'] = 'java'", + prettierConfigPluginsJavaStr, + "def prettierPackagesJava = [:]", + "prettierPackagesJava['prettier'] = '" + prettierVersion + "'", + "prettierPackagesJava['prettier-plugin-java'] = '" + prettierPluginJava + "'", + "spotless {", + " format 'php', {", + " target 'php-example.php'", + " prettier(prettierPackagesPhp).config(prettierConfigPhp)", + " }", + " java {", + " target 'JavaTest.java'", + " prettier(prettierPackagesJava).config(prettierConfigJava)", + " }", + "}"); + setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + final BuildResult spotlessApply = gradleRunner().forwardOutput().withArguments("--stacktrace", "--info", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + final BuildResult spotlessApply2 = gradleRunner().forwardOutput().withArguments("--stacktrace", "--info", "spotlessApply").build(); + Assertions.assertThat(spotlessApply2.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); + assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); + } + + @ParameterizedTest(name = "{index}: autodetectNpmrcFileConfig with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void autodetectNpmrcFileConfig(String prettierVersion) throws IOException { setFile(".npmrc").toLines( - "registry=https://i.do.no.exist.com"); + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -175,7 +350,7 @@ void autodetectNpmrcFileConfig() throws IOException { "spotless {", " format 'mytypescript', {", " target 'test.ts'", - " prettier().config(prettierConfig)", + " prettier('" + prettierVersion + "').config(prettierConfig)", " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); @@ -183,10 +358,62 @@ void autodetectNpmrcFileConfig() throws IOException { Assertions.assertThat(spotlessApply.getOutput()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); } - @Test - void pickupNpmrcFileConfig() throws IOException { + @ParameterizedTest(name = "{index}: verifyCleanAndSpotlessWorks with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void verifyCleanAndSpotlessWorks(String prettierVersion) throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 20", + "prettierConfig['parser'] = 'typescript'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier('" + prettierVersion + "').config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "clean", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + final BuildResult spotlessApply2 = gradleRunner().withArguments("--stacktrace", "clean", "spotlessApply").build(); + Assertions.assertThat(spotlessApply2.getOutput()).contains("BUILD SUCCESSFUL"); + } + + @ParameterizedTest(name = "{index}: verifyCleanAndSpotlessWithNpmInstallCacheWorks with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void verifyCleanAndSpotlessWithNpmInstallCacheWorks(String prettierVersion) throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 20", + "prettierConfig['parser'] = 'typescript'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier('" + prettierVersion + "').npmInstallCache().config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "clean", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + final BuildResult spotlessApply2 = gradleRunner().withArguments("--stacktrace", "clean", "spotlessApply").build(); + Assertions.assertThat(spotlessApply2.getOutput()).contains("BUILD SUCCESSFUL"); + } + + @ParameterizedTest(name = "{index}: autodetectNpmrcFileConfig with prettier {0}") + @ValueSource(strings = {PRETTIER_VERSION_2, PRETTIER_VERSION_3}) + void pickupNpmrcFileConfig(String prettierVersion) throws IOException { setFile(".custom_npmrc").toLines( - "registry=https://i.do.no.exist.com"); + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -198,7 +425,7 @@ void pickupNpmrcFileConfig() throws IOException { "spotless {", " format 'mytypescript', {", " target 'test.ts'", - " prettier().npmrc('.custom_npmrc').config(prettierConfig)", + " prettier('" + prettierVersion + "').npmrc('.custom_npmrc').config(prettierConfig)", " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java index 480de4f01b..76d0449060 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,11 +32,11 @@ void duplicateConfigs() throws IOException { "spotless {", " java {", " target 'src/main/java/**/*.java'", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", " format 'javaDupe', com.diffplug.gradle.spotless.JavaExtension, {", " target 'src/boop/java/**/*.java'", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java new file mode 100644 index 0000000000..2860325132 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ShellExtensionTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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; + +import com.diffplug.spotless.tag.ShfmtTest; + +@ShfmtTest +public class ShellExtensionTest extends GradleIntegrationHarness { + + @Test + void shfmtWithEditorconfig() throws IOException { + String fileDir = "shell/shfmt/with-config/"; + + setFile("build.gradle.kts").toLines( + "plugins {", + " id(\"com.diffplug.spotless\")", + "}", + "spotless {", + " shell {", + " shfmt()", + " }", + "}"); + + setFile(".editorconfig").toResource(fileDir + ".editorconfig"); + + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } + + @Test + void shfmtWithoutEditorconfig() throws IOException { + String fileDir = "shell/shfmt/without-config/"; + + setFile("build.gradle.kts").toLines( + "plugins {", + " id(\"com.diffplug.spotless\")", + "}", + "spotless {", + " shell {", + " shfmt()", + " }", + "}"); + + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SortPomGradleTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SortPomGradleTest.java new file mode 100644 index 0000000000..80c8f254f2 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SortPomGradleTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2024 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 org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SortPomGradleTest extends GradleIntegrationHarness { + @Test + void sortPom() throws Exception { + // given + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " pom {", + " sortPom()", + " }", + "}"); + setFile("pom.xml").toResource("pom/pom_dirty.xml"); + + // when + gradleRunner().withArguments("spotlessApply").build(); + + // then + assertFile("pom.xml").sameAsResource("pom/pom_clean_default.xml"); + } + + @Test + void sortPomWithTarget() throws Exception { + // given + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " pom {", + " target('test.xml')", + " sortPom()", + " }", + "}"); + setFile("test.xml").toResource("pom/pom_dirty.xml"); + + // when + gradleRunner().withArguments("spotlessApply").build(); + + // then + assertFile("test.xml").sameAsResource("pom/pom_clean_default.xml"); + } + + @ParameterizedTest + @ValueSource(strings = {"3.2.1", "3.3.0", "3.4.1", "4.0.0"}) + void sortPomWithVersion(String version) throws Exception { + // given + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " pom {", + " sortPom '" + version + "'", + " }", + "}"); + setFile("pom.xml").toResource("pom/pom_dirty.xml"); + + // when + gradleRunner().withArguments("spotlessApply").build(); + + // then + assertFile("pom.xml").sameAsResource("pom/pom_clean_default.xml"); + } + + @Test + void sortPomWithParameters() throws Exception { + // given + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " pom {", + " sortPom()", + " .encoding('UTF-8')", + " .lineSeparator(System.getProperty('line.separator'))", + " .expandEmptyElements(true)", + " .spaceBeforeCloseEmptyElement(false)", + " .keepBlankLines(true)", + " .endWithNewline(true)", + " .nrOfIndentSpace(2)", + " .indentBlankLines(false)", + " .indentSchemaLocation(false)", + " .indentAttribute(null)", + " .predefinedSortOrder('recommended_2008_06')", + " .sortOrderFile(null)", + " .sortDependencies(null)", + " .sortDependencyManagement(null)", + " .sortDependencyExclusions(null)", + " .sortPlugins(null)", + " .sortProperties(false)", + " .sortModules(false)", + " .sortExecutions(false)", + " }", + "}"); + setFile("pom.xml").toResource("pom/pom_dirty.xml"); + + // when + gradleRunner().withArguments("spotlessApply").build(); + + // then + assertFile("pom.xml").sameAsResource("pom/pom_clean_default.xml"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPluginRedirectTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPluginRedirectTest.java index acb57f3763..a427d6f2ec 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPluginRedirectTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPluginRedirectTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ void redirectPluginOldGradle() throws IOException { " > We have moved from 'com.diffplug.gradle.spotless'", " to 'com.diffplug.spotless'", " To migrate:", - " - Upgrade gradle to 6.1.1 or newer (you're on 5.0)", + " - Upgrade Gradle to 6.1.1 or newer (you're on 5.0)", " - Test your build with: id 'com.diffplug.gradle.spotless' version '4.5.1'")); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessTaskImplTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessTaskImplTest.java new file mode 100644 index 0000000000..a1f49c7373 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessTaskImplTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023-2024 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.File; +import java.nio.file.Paths; + +import org.assertj.core.api.Assertions; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.logging.Logger; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.diffplug.spotless.Formatter; + +public class SpotlessTaskImplTest { + @Test + public void testThrowsMessageContainsFilename() throws Exception { + SpotlessTaskImpl task = Mockito.mock(SpotlessTaskImpl.class, Mockito.CALLS_REAL_METHODS); + Mockito.when(task.getLogger()).thenReturn(Mockito.mock(Logger.class)); + + File projectDir = Paths.get("unitTests", "projectDir").toFile(); + DirectoryProperty projectDirProperty = Mockito.mock(DirectoryProperty.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(projectDirProperty.get().getAsFile()).thenReturn(projectDir); + + Mockito.when(task.getProjectDir()).thenReturn(projectDirProperty); + + File input = Paths.get("unitTests", "projectDir", "someInput").toFile(); + Formatter formatter = Mockito.mock(Formatter.class); + + Assertions.assertThatThrownBy(() -> task.processInputFile(null, formatter, input, "someInput")).hasMessageContaining(input.toString()); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java new file mode 100644 index 0000000000..f1a2cf4298 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-2023 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 TargetExcludeIfContentContainsTest extends GradleIntegrationHarness { + @Test + void targetExcludeIfContentContainsWithOneValue() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContains '// Generated by Mr. Roboto'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + String content = "// Generated by Mr. Roboto, do not edit.\n" + + "A B C\n" + + "D E F\n" + + "G H I"; + setFile("test_generated.md").toContent(content); + setFile("test_manual.md").toLines( + "A B C", + "D E F", + "G H I"); + gradleRunner().withArguments("spotlessApply").build(); + // `test_generated` contains the excluding text so didn't change. + assertFile("test_generated.md").hasContent(content); + // `test_manual` does not so it changed. + assertFile("test_manual.md").hasLines( + "a b c", + "d e f", + "g h i"); + } + + @Test + void targetExcludeIfContentContainsWithMultipleSteps() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContains '// Generated by Mr. Roboto'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " licenseHeader('" + "// My CopyRights header" + "', '--')", + " }", + "}"); + String generatedContent = "// Generated by Mr. Roboto, do not edit.\n" + + "--\n" + + "public final class MyMessage {}\n"; + setFile("test_generated.md").toContent(generatedContent); + String manualContent = "// Typo in License\n" + + "--\n" + + "public final class MyMessage {\n" + + "}"; + setFile("test_manual.md").toContent(manualContent); + gradleRunner().withArguments("spotlessApply").build(); + + // `test_generated` contains the excluding text so didn't change, including the header. + assertFile("test_generated.md").hasContent(generatedContent); + // `test_manual` does not, it changed. + assertFile("test_manual.md").hasContent( + "// My CopyRights header\n" + + "--\n" + + "public final class mymessage {\n" + + "}"); + } + + @Test + void targetExcludeIfContentContainsRegex() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContainsRegex '// Generated by Mr. Roboto|// Generated by Mrs. Call'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + String robotoContent = "A B C\n" + + "// Generated by Mr. Roboto, do not edit.\n" + + "D E F\n" + + "G H I"; + setFile("test_generated_roboto.md").toContent(robotoContent); + String callContent = "A B C\n" + + "D E F\n" + + "// Generated by Mrs. Call, do not edit.\n" + + "G H I"; + setFile("test_generated_call.md").toContent(callContent); + String collaborationContent = "A B C\n" + + "// Generated by Mr. Roboto, do not edit.\n" + + "D E F\n" + + "// Generated by Mrs. Call, do not edit.\n" + + "G H I"; + setFile("test_generated_collaboration.md").toContent(collaborationContent); + String intruderContent = "A B C\n" + + "// Generated by K2000, do not edit.\n" + + "D E F\n" + + "G H I"; + setFile("test_generated_intruder.md").toContent(intruderContent); + setFile("test_manual.md").toLines( + "A B C", + "D E F", + "G H I"); + gradleRunner().withArguments("spotlessApply").build(); + // Part of the excluding values so has not changed. + assertFile("test_generated_roboto.md").hasContent(robotoContent); + // Part of the excluding values so has not changed. + assertFile("test_generated_call.md").hasContent(callContent); + // Part of the excluding values so has not changed. + assertFile("test_generated_collaboration.md").hasContent(collaborationContent); + // Not part of the excluding values so has changed. + assertFile("test_generated_intruder.md").hasContent( + "a b c\n" + + "// generated by k2000, do not edit.\n" + + "d e f\n" + + "g h i"); + // `test_manual` does not, it changed. + assertFile("test_manual.md").hasLines( + "a b c", + "d e f", + "g h i"); + } +} 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 9b9e887749..11850a5cff 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-2021 DiffPlug + * Copyright 2020-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,48 @@ import java.io.IOException; +import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.Test; -class ToggleOffOnTest extends GradleIntegrationHarness { +abstract class ToggleOffOnTest extends GradleIntegrationHarness { + private boolean useConfigCache; + + ToggleOffOnTest(boolean useConfigCache) { + this.useConfigCache = useConfigCache; + } + + static class WithConfigCache extends ToggleOffOnTest { + WithConfigCache() { + super(true); + } + } + + static class WithoutConfigCache extends ToggleOffOnTest { + WithoutConfigCache() { + super(false); + } + } + + @Override + public GradleRunner gradleRunner() throws IOException { + if (useConfigCache) { + setFile("gradle.properties").toLines("org.gradle.unsafe.configuration-cache=true", + "org.gradle.configuration-cache=true"); + return super.gradleRunner().withGradleVersion(GradleVersionSupport.STABLE_CONFIGURATION_CACHE.version); + } else { + return super.gradleRunner(); + } + } + @Test - void toggleOffOn() throws IOException { + void lowercase() throws IOException { setFile("build.gradle").toLines( "plugins { id 'com.diffplug.spotless' }", "spotless {", + " lineEndings 'UNIX'", " format 'toLower', {", " target '**/*.md'", - " custom 'lowercase', { str -> str.toLowerCase() }", + " addStep(com.diffplug.spotless.TestingOnly.lowercase())", " toggleOffOn()", " }", "}"); @@ -37,7 +68,23 @@ void toggleOffOn() throws IOException { "D E F", "spotless:on", "G H I"); - gradleRunner().withArguments("spotlessApply").build(); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + assertFile("test.md").hasLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i"); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + setFile("test.md").toLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I"); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); assertFile("test.md").hasLines( "a b c", "spotless:off", @@ -45,4 +92,26 @@ void toggleOffOn() throws IOException { "spotless:on", "g h i"); } + + @Test + void gjf() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0')", + " toggleOffOn()", + " }", + "}"); + + setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.java").sameAsResource("java/googlejavaformat/JavaCodeFormatted.test"); + gradleRunner().withArguments("spotlessCheck").build(); + } } 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 100c4eb3b0..0882d0f944 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,20 @@ import java.io.IOException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintStyleGuide; import com.diffplug.spotless.tag.NpmTest; @NpmTest class TypescriptExtensionTest extends GradleIntegrationHarness { + + private static String styleGuideMapString(String styleGuideName) { + return EslintStyleGuide.fromNameOrNull(styleGuideName).asGradleMapStringMergedWith(EslintFormatterStep.defaultDevDependencies()); + } + @Test void allowToSpecifyFormatterVersion() throws IOException { setFile("build.gradle").toLines( @@ -141,4 +149,64 @@ void usePrettier() throws IOException { gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); assertFile("test.ts").sameAsResource("npm/prettier/filetypes/typescript/typescript.clean"); } + + @Test + void useEslint() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/typescript/custom_rules/.eslintrc.js"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " eslint().configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.ts").toResource("npm/eslint/typescript/custom_rules/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/eslint/typescript/custom_rules/typescript.clean"); + } + + @Test + @Disabled + void useEslintXoStandardRules() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/typescript/styleguide/xo/.eslintrc.js"); + setFile("tsconfig.json").toResource("npm/eslint/typescript/styleguide/xo/tsconfig.json"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " eslint(" + styleGuideMapString("xo-typescript") + ").configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.ts").toResource("npm/eslint/typescript/styleguide/xo/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/eslint/typescript/styleguide/xo/typescript.clean"); + } + + @Test + void useEslintStandardWithTypescriptRules() throws IOException { + setFile(".eslintrc.js").toResource("npm/eslint/typescript/styleguide/standard_with_typescript/.eslintrc.js"); + setFile("tsconfig.json").toResource("npm/eslint/typescript/styleguide/standard_with_typescript/tsconfig.json"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target 'test.ts'", + " eslint(" + styleGuideMapString("standard-with-typescript") + ").configFile('.eslintrc.js')", + " }", + "}"); + setFile("test.ts").toResource("npm/eslint/typescript/styleguide/standard_with_typescript/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean"); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/WithinBlockTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/WithinBlockTest.java index 01fb048b66..d1df8b5f96 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/WithinBlockTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/WithinBlockTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ void genericFormatTest() throws IOException { "spotless {", " format 'customJava', JavaExtension, {", " target '*.java'", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", "}"); setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); @@ -53,7 +53,7 @@ void withinBlocksTourDeForce() throws IOException { " custom 'lowercase', { str -> str.toLowerCase() }", " }", " withinBlocks 'java only', '\\n```java\\n', '\\n```\\n', JavaExtension, {", - " googleJavaFormat('1.2')", + " googleJavaFormat()", " }", " }", "}"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java new file mode 100644 index 0000000000..067c809846 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021-2023 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 YamlExtensionTest extends GradleIntegrationHarness { + @Test + void testFormatYaml_WithJackson_defaultConfig_separatorComments() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " yaml {", + " target 'src/**/*.yaml'", + " jackson()", + " }", + "}"); + setFile("src/main/resources/example.yaml").toResource("yaml/separator_comments.yaml"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/resources/example.yaml").sameAsResource("yaml/separator_comments.clean.yaml"); + } + + // see YAMLGenerator.Feature.WRITE_DOC_START_MARKER + @Test + void testFormatYaml_WithJackson_skipDocStartMarker() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " yaml {", + " target 'src/**/*.yaml'", + " jackson()", + " .yamlFeature('WRITE_DOC_START_MARKER', false)", + " .yamlFeature('MINIMIZE_QUOTES', true)", + " }", + "}"); + setFile("src/main/resources/example.yaml").toResource("yaml/array_with_bracket.yaml"); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + assertFile("src/main/resources/example.yaml").sameAsResource("yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml"); + } + + @Test + void testFormatYaml_WithJackson_multipleDocuments() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " yaml {", + " target 'src/**/*.yaml'", + " jackson()", + " }", + "}"); + setFile("src/main/resources/example.yaml").toResource("yaml/multiple_documents.yaml"); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + assertFile("src/main/resources/example.yaml").sameAsResource("yaml/multiple_documents.clean.jackson.yaml"); + } + + @Test + void testFormatYaml_WithJackson_arrayAtRoot() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " yaml {", + " target 'src/**/*.yaml'", + " jackson()", + " }", + "}"); + setFile("src/main/resources/example.yaml").toResource("yaml/array_at_root.yaml"); + gradleRunner().withArguments("spotlessApply", "--stacktrace").build(); + assertFile("src/main/resources/example.yaml").sameAsResource("yaml/array_at_root.clean.yaml"); + } + +} 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 new file mode 100644 index 0000000000..c0fa255bcc --- /dev/null +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_1.gradle @@ -0,0 +1,38 @@ +/** + * This example shows how to use the gradle-node-plugin to install node and npm in separate directories + * and use these binaries with the prettier formatter. + */ +plugins { + id 'com.diffplug.spotless' + id 'com.github.node-gradle.node' version '3.5.1' +} +repositories { mavenCentral() } +node { + download = true + version = '18.16.1' + npmVersion = '9.5.1' + // when setting both these directories, npm and node will be in separate directories + workDir = file("${buildDir}/nodejs") + npmWorkDir = file("${buildDir}/npm") +} +def prettierConfig = [:] +prettierConfig['printWidth'] = 20 +prettierConfig['parser'] = 'typescript' + +// the executable names +def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm' +def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node' + +spotless { + format 'mytypescript', { + target 'test.ts' + prettier() + .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") // get the npm executable path from gradle-node-plugin + .nodeExecutable("${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}") // get the node executable path from gradle-node-plugin + .config(prettierConfig) + } +} + +tasks.named('spotlessMytypescript').configure { + it.dependsOn('nodeSetup', 'npmSetup') +} 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 new file mode 100644 index 0000000000..6d77bc3d69 --- /dev/null +++ b/plugin-gradle/src/test/resources/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest_gradle_node_plugin_example_2.gradle @@ -0,0 +1,35 @@ +/** + * This example shows how to use the gradle-node-plugin to install node and npm together and + * use these binaries with the prettier formatter. + */ +plugins { + id 'com.diffplug.spotless' + id 'com.github.node-gradle.node' version '3.5.1' +} +repositories { mavenCentral() } +node { + download = true + version = '18.16.1' + // when not setting an explicit `npmWorkDir`, the npm binary will be installed next to the node binary + workDir = file("${buildDir}/nodejs") +} +def prettierConfig = [:] +prettierConfig['printWidth'] = 20 +prettierConfig['parser'] = 'typescript' + +// the executable name +def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm' + +spotless { + typescript { + target 'test.ts' + prettier() + .npmExecutable("${tasks.named('npmSetup').get().npmDir.get()}${npmExec}") // get the npm executable path from gradle-node-plugin + // setting the nodeExecutable is not necessary, since it will be found in the same directory as the npm executable (see above) + .config(prettierConfig) + } +} + +tasks.named('spotlessTypescript').configure { + it.dependsOn('nodeSetup', 'npmSetup') +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index c5ca50d936..abf33d63fb 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,9 +3,456 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] + +## [2.44.4] - 2025-04-07 +### Changed +* Use palantir-java-format 2.57.0 on Java 21. ([#2447](https://github.com/diffplug/spotless/pull/2447)) +* Re-try `npm install` with `--prefer-online` after `ERESOLVE` error. ([#2448](https://github.com/diffplug/spotless/pull/2448)) + +## [2.44.3] - 2025-02-20 +* Support for `clang-format` ([#2406](https://github.com/diffplug/spotless/pull/2406)) +* Adopt new version of `maven-plugin-development` from GradleX ([#2395](https://github.com/diffplug/spotless/pull/2395)) + +## [2.44.2] - 2025-01-14 +* Eclipse-based tasks can now handle parallel configuration ([#2389](https://github.com/diffplug/spotless/issues/2389)) + +## [2.44.1] - 2025-01-07 +### Fixed +* Deployment was missing part of the CDT formatter, now fixed. ([#2384](https://github.com/diffplug/spotless/issues/2384)) + +## [2.44.0] - 2025-01-06 +## Headline changes +* The long `2.44.0.BETAX` period is finally over (sorry, there was [a problem in Gradle land](https://github.com/diffplug/spotless/issues/987)). +* Spotless now supports [linting](https://github.com/diffplug/spotless/blob/main/CONTRIBUTING.md#lints) in addition to formatting. + * help wanted: [maven api to suppress lints](https://github.com/diffplug/spotless/issues/2309) +### Changed +* Bump default `ktlint` version to latest `1.3.0` -> `1.4.0`. ([#2314](https://github.com/diffplug/spotless/pull/2314)) +* Bump default `jackson` version to latest `2.18.0` -> `2.18.1`. ([#2319](https://github.com/diffplug/spotless/pull/2319)) +* Bump default `ktfmt` version to latest `0.52` -> `0.53`. ([#2320](https://github.com/diffplug/spotless/pull/2320)) +* Bump default `ktlint` version to latest `1.4.0` -> `1.5.0`. ([#2354](https://github.com/diffplug/spotless/pull/2354)) +* Bump minimum `eclipse-cdt` version to `11.0` (removed support for `10.7`). ([#2373](https://github.com/diffplug/spotless/pull/2373)) +* Bump default `eclipse` version to latest `4.32` -> `4.34`. ([#2381](https://github.com/diffplug/spotless/pull/2381)) +### Fixed +* You can now use `removeUnusedImports` and `googleJavaFormat` at the same time again. (fixes [#2159](https://github.com/diffplug/spotless/issues/2159)) +* The default list of type annotations used by `formatAnnotations` now includes Jakarta Validation's `Valid` and constraints validations (fixes [#2334](https://github.com/diffplug/spotless/issues/2334)) + +## [2.44.0.BETA4] - 2024-10-24 +### Added +* Support for line ending policy `PRESERVE` which just takes the first line ending of every given file as setting (no matter if `\n`, `\r\n` or `\r`) ([#2304](https://github.com/diffplug/spotless/pull/2304)) +### Fixed +* `ktlint` steps now read from the `string` instead of the `file` so they don't clobber earlier steps. (fixes [#1599](https://github.com/diffplug/spotless/issues/1599)) + +## [2.44.0.BETA3] - 2024-10-15 +### Added +* Support for `rdf` ([#2261](https://github.com/diffplug/spotless/pull/2261)) +* Support for `buf` ([#2291](https://github.com/diffplug/spotless/pull/2291)) +### Changed +* Leverage local repository for Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238)) +* Add explicit support for CSS via biome. Formatting CSS via biome was already supported as a general + formatting step. Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). + ([#2259](https://github.com/diffplug/spotless/pull/2259)) +* Bump default `google-java-format` version to latest `1.23.0` -> `1.24.0`. ([#2294](https://github.com/diffplug/spotless/pull/2294)) +* Bump default `jackson` version to latest `2.17.2` -> `2.18.0`. ([#2279](https://github.com/diffplug/spotless/pull/2279)) +* Bump default `cleanthat` version to latest `2.21` -> `2.22`. ([#2296](https://github.com/diffplug/spotless/pull/2296)) +### Fixed +* Java import order, ignore duplicate group entries. ([#2293](https://github.com/diffplug/spotless/pull/2293)) + +## [2.44.0.BETA2] - 2024-08-25 +### Changed +* Support toning down sortPom logging. ([#2185](https://github.com/diffplug/spotless/pull/2185)) +* Bump default `ktlint` version to latest `1.2.1` -> `1.3.0`. ([#2165](https://github.com/diffplug/spotless/pull/2165)) +* Bump default `ktfmt` version to latest `0.49` -> `0.52`. ([#2172](https://github.com/diffplug/spotless/pull/2172), [#2231](https://github.com/diffplug/spotless/pull/2231)) +* Rename property `ktfmt` option `removeUnusedImport` -> `removeUnusedImports` to match `ktfmt`. ([#2172](https://github.com/diffplug/spotless/pull/2172)) +* Bump default `eclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `greclipse` version to latest `4.29` -> `4.32`. ([#2179](https://github.com/diffplug/spotless/pull/2179), [#2190](https://github.com/diffplug/spotless/pull/2190)) +* Bump default `cdt` version to latest `11.3` -> `11.6`. ([#2179](https://github.com/diffplug/spotless/pull/2179)) +* Bump default `gson` version to latest `2.10.1` -> `2.11.0`. ([#2128](https://github.com/diffplug/spotless/pull/2128)) +* Bump default `jackson` version to latest `2.17.1` -> `2.17.2`. ([#2195](https://github.com/diffplug/spotless/pull/2195)) +* Bump default `cleanthat` version to latest `2.20` -> `2.21`. ([#2210](https://github.com/diffplug/spotless/pull/2210)) +* Bump default `google-java-format` version to latest `1.22.0` -> `1.23.0`. ([#2212](https://github.com/diffplug/spotless/pull/2212)) +### Fixed +* Fix compatibility issue introduced by `ktfmt` `0.51`. ([#2172](https://github.com/diffplug/spotless/issues/2172)) +### Added +* Add option `manageTrailingCommas` to `ktfmt`. ([#2177](https://github.com/diffplug/spotless/pull/2177)) + +## [2.44.0.BETA1] - 2024-06-04 +### Added +* Respect `.editorconfig` settings for formatting shell via `shfmt` ([#2031](https://github.com/diffplug/spotless/pull/2031)) +* Skip execution in M2E (incremental) builds by default ([#1814](https://github.com/diffplug/spotless/issues/1814), [#2037](https://github.com/diffplug/spotless/issues/2037)) +### Fixed +* Check if ktlint_code_style is set in .editorconfig before overriding it ([#2143](https://github.com/diffplug/spotless/issues/2143)) +* Default EditorConfig path to ".editorconfig" ([#2143](https://github.com/diffplug/spotless/issues/2143)) +* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990)) +* Correctly provide EditorConfig property types for Ktlint ([#2052](https://github.com/diffplug/spotless/issues/2052)) +* Made ShadowCopy (`npmInstallCache`) more robust by re-creating the cache dir if it goes missing ([#1984](https://github.com/diffplug/spotless/issues/1984),[2096](https://github.com/diffplug/spotless/pull/2096)) +* scalafmt.conf fileOverride section now works correctly ([#1854](https://github.com/diffplug/spotless/pull/1854)) +* Fix stdin pipe is being closed exception on Windows for large .proto files ([#2147](https://github.com/diffplug/spotless/issues/2147)) +* Reworked ShadowCopy (`npmInstallCache`) to use atomic filesystem operations, resolving several race conditions that could arise ([#2151](https://github.com/diffplug/spotless/pull/2151)) +### Changed +* Bump default `cleanthat` version to latest `2.16` -> `2.20`. ([#1725](https://github.com/diffplug/spotless/pull/1725)) +* Bump default `gherkin-utils` version to latest `8.0.2` -> `9.0.0`. ([#1703](https://github.com/diffplug/spotless/pull/1703)) +* Bump default `google-java-format` version to latest `1.19.2` -> `1.22.0`. ([#2129](https://github.com/diffplug/spotless/pull/2129)) +* Bump default `jackson` version to latest `2.14.2` -> `2.17.1`. ([#1685](https://github.com/diffplug/spotless/pull/1685)) +* Bump default `ktfmt` version to latest `0.46` -> `0.49`. ([#2045](https://github.com/diffplug/spotless/pull/2045), [#2127](https://github.com/diffplug/spotless/pull/2127)) +* Bump default `ktlint` version to latest `1.1.1` -> `1.2.1`. ([#2057](https://github.com/diffplug/spotless/pull/2057)) +* Bump default `scalafmt` version to latest `3.7.3` -> `3.8.1`. ([#1730](https://github.com/diffplug/spotless/pull/1730)) +* Bump default `shfmt` version to latest `3.7.0` -> `3.8.0`. ([#2050](https://github.com/diffplug/spotless/pull/2050)) +* Bump default `sortpom` version to latest `3.2.1` -> `4.0.0`. ([#2049](https://github.com/diffplug/spotless/pull/2049), [#2078](https://github.com/diffplug/spotless/pull/2078), [#2115](https://github.com/diffplug/spotless/pull/2115)) +* Bump default `zjsonpatch` version to latest `0.4.14` -> `0.4.16`. ([#1969](https://github.com/diffplug/spotless/pull/1969)) +### Removed +* **BREAKING** Fully removed `Rome`, use `Biome` instead. ([#2119](https://github.com/diffplug/spotless/pull/2119)) + +## [2.43.0] - 2024-01-23 +### Added +* Support for formatting shell scripts via [shfmt](https://github.com/mvdan/sh). ([#1998](https://github.com/diffplug/spotless/issues/1998)) +* Support for `gofmt` ([#2001](https://github.com/diffplug/spotless/pull/2001)) +* Support for formatting Java Docs for the Palantir formatter ([#2009](https://github.com/diffplug/spotless/pull/2009)) + +## [2.42.0] - 2024-01-15 +### Added +* M2E support: Emit file specific errors during incremental build. ([#1960](https://github.com/diffplug/spotless/issues/1960)) +### Fixed +* Fix empty files with biome >= 1.5.0 when formatting files that are in the ignore list of the biome configuration file. ([#1989](https://github.com/diffplug/spotless/pull/1989) fixes [#1987](https://github.com/diffplug/spotless/issues/1987)) +### Changed +* Use palantir-java-format 2.39.0 on Java 21. ([#1948](https://github.com/diffplug/spotless/pull/1948)) +* Bump default `ktlint` version to latest `1.0.1` -> `1.1.1`. ([#1973](https://github.com/diffplug/spotless/pull/1973)) +* Bump default `googleJavaFormat` version to latest `1.18.1` -> `1.19.2`. ([#1971](https://github.com/diffplug/spotless/pull/1971)) +* Bump default `diktat` version to latest `1.2.5` -> `2.0.0`. ([#1972](https://github.com/diffplug/spotless/pull/1972)) + +## [2.41.1] - 2023-12-04 +### Fixed +* Revert [#1846](https://github.com/diffplug/spotless/issues/1846) from 2.41.0 which causes the plugin to format generated sources in the `target` directory. ([#1928](https://github.com/diffplug/spotless/pull/1928)) +* Eclipse-based steps which contained any jars with a `+` in their path were broken, now fixed. ([#1860](https://github.com/diffplug/spotless/issues/1860#issuecomment-1826113332)) +### Changed +* Bump default `palantir-java-format` version to latest `2.28.0` -> `2.38.0` on Java 21. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `googleJavaFormat` version to latest `1.17.0` -> `1.18.1`. ([#1920](https://github.com/diffplug/spotless/pull/1920)) +* Bump default `ktfmt` version to latest `0.44` -> `0.46`. ([#1927](https://github.com/diffplug/spotless/pull/1927)) +* Bump default `eclipse` version to latest `4.27` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `greclipse` version to latest `4.28` -> `4.29`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) +* Bump default `cdt` version to latest `11.1` -> `11.3`. ([#1939](https://github.com/diffplug/spotless/pull/1939)) + +## [2.41.0] - 2023-11-27 +### Added +* ~~CompileSourceRoots and TestCompileSourceRoots are now respected as default includes. These properties are commonly set when adding extra source directories.~~ ([#1846](https://github.com/diffplug/spotless/issues/1846)) + * Reverted in the next release (`2.41.1`) due to backward compatibility problems, see [#1914](https://github.com/diffplug/spotless/issues/1914). +* Support custom rule sets for Ktlint. ([#1896](https://github.com/diffplug/spotless/pull/1896)) +### Fixed +* Fix crash when build dir is a softlink to another directory. ([#1859](https://github.com/diffplug/spotless/pull/1859)) +* Fix Eclipse JDT on some settings files. ([#1864](https://github.com/diffplug/spotless/pull/1864) fixes [#1638](https://github.com/diffplug/spotless/issues/1638)) +### Changed +* Bump default `ktlint` version to latest `1.0.0` -> `1.0.1`. ([#1855](https://github.com/diffplug/spotless/pull/1855)) +* Add a Step to remove semicolons from Groovy files. ([#1881](https://github.com/diffplug/spotless/pull/1881)) + +## [2.40.0] - 2023-09-28 +### Added +* Add `-DspotlessIdeHook` that provides the ability to apply Spotless exclusively to a specified file. It accepts the absolute path of the file. ([#1782](https://github.com/diffplug/spotless/pull/1782)) + * BETA, subject to change until we have proven compatibility with some IDE plugins. +* Added support for `google-java-format`'s `skip-javadoc-formatting` option ([#1793](https://github.com/diffplug/spotless/pull/1793)) +* Added support for biome. The Rome project [was renamed to Biome](https://biomejs.dev/blog/annoucing-biome/). + The configuration is still the same, but you should switch to the new `` tag and adjust + the version accordingly. ([#1804](https://github.com/diffplug/spotless/issues/1804)). +* Support configuration of mirrors for P2 repositories ([#1697](https://github.com/diffplug/spotless/issues/1697)): + ``` + + + + https://download.eclipse.org/ + https://some.internal.mirror/eclipse + + + + ``` + Mirrors are selected by prefix match, for example `https://download.eclipse.org/eclipse/updates/4.26/` will be redirected to `https://some.internal.mirror/eclipse/eclipse/updates/4.26/`. + The same configuration exists for `` and ``. +### Fixed +* Fixed support for plugins when using Prettier version `3.0.0` and newer. ([#1802](https://github.com/diffplug/spotless/pull/1802)) +### Changed +* Bump default `flexmark` version to latest `0.64.0` -> `0.64.8`. ([#1801](https://github.com/diffplug/spotless/pull/1801)) +* Bump default `ktlint` version to latest `0.50.0` -> `1.0.0`. ([#1808](https://github.com/diffplug/spotless/pull/1808)) +* **POSSIBLY BREAKING** the default line endings are now `GIT_ATTRIBUTES_FAST_ALLSAME` instead of `GIT_ATTRIBUTES`. ([#1838](https://github.com/diffplug/spotless/pull/1838)) + * If all the files within a format have the same line endings, then there is no change in behavior. + * Fixes large performance regression. ([#1527](https://github.com/diffplug/spotless/issues/1527)) + +## [2.39.0] - 2023-08-29 +### Added +* Add a `jsonPatch` step to `json` formatter configurations. This allows patching of JSON documents using [JSON Patches](https://jsonpatch.com). ([#1753](https://github.com/diffplug/spotless/pull/1753)) +* Support GJF own import order. ([#1780](https://github.com/diffplug/spotless/pull/1780)) +### Fixed +* Add support for `prettier` version `3.0.0` and newer. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1751](https://github.com/diffplug/spotless/issues/1751)) +* Fix npm install calls when npm cache is not up-to-date. ([#1760](https://github.com/diffplug/spotless/pull/1760), [#1750](https://github.com/diffplug/spotless/issues/1750)) +### Changed +* Bump default `eslint` version to latest `8.31.0` -> `8.45.0` ([#1761](https://github.com/diffplug/spotless/pull/1761)) +* Bump default `prettier` version to latest (v2) `2.8.1` -> `2.8.8`. ([#1760](https://github.com/diffplug/spotless/pull/1760)) +* Bump default `greclipse` version to latest `4.27` -> `4.28`. ([#1775](https://github.com/diffplug/spotless/pull/1775)) + +## [2.38.0] - 2023-07-17 +### Added +* Support pass skip (`-Dspotless.skip=true`) from command-line. ([#1729](https://github.com/diffplug/spotless/pull/1729)) +### Fixed +* Update documented default `semanticSort` to `false`. ([#1728](https://github.com/diffplug/spotless/pull/1728)) +### Changed +* Bump default `cleanthat` version to latest `2.13` -> `2.17`. ([#1734](https://github.com/diffplug/spotless/pull/1734)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`. ([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. + * Dropped support for deprecated `useExperimental` parameter in favor of the `ktlint_experimental` property. + +## [2.37.0] - 2023-05-24 +### Added +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) +* Add semantics-aware Java import ordering (i.e. sort by package, then class, then member). ([#522](https://github.com/diffplug/spotless/issues/522)) +### Fixed +* `palantir` step now accepts a `style` parameter, which is documentation had already claimed to do. ([#1694](https://github.com/diffplug/spotless/pull/1694)) +* Fixed a regression which changed the import sorting order in `googleJavaFormat` introduced in `2.36.0`. ([#1680](https://github.com/diffplug/spotless/pull/1680)) +* Equo-based formatters now work on platforms unsupported by Eclipse such as PowerPC (fixes [durian-swt#20](https://github.com/diffplug/durian-swt/issues/20)) +* When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) +### Changed +* Equo-based formatters now download metadata to `~/.m2/repository/dev/equo/p2-data` rather than `~/.equo`, and for CI machines without a home directory the p2 data goes to `$GRADLE_USER_HOME/caches/p2-data`. ([#1714](https://github.com/diffplug/spotless/pull/1714)) +* Bump default `googleJavaFormat` version to latest `1.16.0` -> `1.17.0`. ([#1710](https://github.com/diffplug/spotless/pull/1710)) +* Bump default `ktfmt` version to latest `0.43` -> `0.44`. ([#1691](https://github.com/diffplug/spotless/pull/1691)) +* Bump default `ktlint` version to latest `0.48.2` -> `0.49.1`. ([#1696](https://github.com/diffplug/spotless/issues/1696)) + * Dropped support for `ktlint 0.46.x` following our policy of supporting two breaking changes at a time. +* Bump default `sortpom` version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) + +## [2.36.0] - 2023-04-06 +### Added +* `removeUnusedImport` can be configured to rely on `cleanthat-javaparser-unnecessaryimport`. Default remains `google-java-format`. ([#1589](https://github.com/diffplug/spotless/pull/1589)) +* The `style` option in Palantir Java Format ([#1654](https://github.com/diffplug/spotless/pull/1654)). +* Added formatter for Gherkin feature files ([#1649](https://github.com/diffplug/spotless/issues/1649)). +### Fixed +* Fix non deterministic computation of cache fingerprint when using multiple formatters. ([#1643](https://github.com/diffplug/spotless/pull/1643) fixes [#1642](https://github.com/diffplug/spotless/pull/1642)) +### Changed +* **POTENTIALLY BREAKING** Drop support for `googleJavaFormat` versions < `1.8`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `cleanthat` version to latest `2.6` -> `2.13`. ([#1589](https://github.com/diffplug/spotless/pull/1589) and [#1661](https://github.com/diffplug/spotless/pull/1661)) +* Bump default `diktat` version `1.2.4.2` -> `1.2.5`. ([#1631](https://github.com/diffplug/spotless/pull/1631)) +* Bump default `flexmark` version `0.62.2` -> `0.64.0`. ([#1302](https://github.com/diffplug/spotless/pull/1302)) +* Bump default `googleJavaFormat` version `1.15.0` -> `1.16.0`. ([#1630](https://github.com/diffplug/spotless/pull/1630)) +* Bump default `scalafmt` version `3.7.1` -> `3.7.3`. ([#1584](https://github.com/diffplug/spotless/pull/1584)) +* Bump default Eclipse formatters for the 2023-03 release. ([#1662](https://github.com/diffplug/spotless/pull/1662)) + * JDT and GrEclipse `4.26` -> `4.27` + * Improve GrEclipse error reporting. ([#1660](https://github.com/diffplug/spotless/pull/1660)) + * CDT `11.0` -> `11.1` + +## [2.35.0] - 2023-03-13 +### Added +* You can now put the filename into a license header template with `$FILE`. ([#1605](https://github.com/diffplug/spotless/pull/1605) fixes [#1147](https://github.com/diffplug/spotless/issues/1147)) +### Fixed +* `licenseHeader` default pattern for Java files is updated to `(package|import|public|class|module) `. ([#1614](https://github.com/diffplug/spotless/pull/1614)) +### Changed +* Enable incremental up-to-date checking by default. ([#1621](https://github.com/diffplug/spotless/pull/1621)) +* All Eclipse formatters are now based on [Equo Solstice OSGi and p2 shim](https://github.com/equodev/equo-ide/tree/main/solstice). ([#1524](https://github.com/diffplug/spotless/pull/1524)) + * Eclipse JDT bumped default to `4.26` from `4.21`, oldest supported is `4.9`. + * We now recommend dropping the last `.0`, e.g. `4.26` instead of `4.26.0`, you'll get warnings to help you switch. + * Eclipse Groovy bumped default to `4.26` from `4.21`, oldest supported is `4.18`. + * Eclipse CDT bumped default to `11.0` from `4.21`, oldest supported is `10.6`. + * Eclipse WTP is still WIP at [#1622](https://github.com/diffplug/spotless/pull/1622). + +## [2.34.0] - 2023-02-27 +### Added +* `cleanthat` added `includeDraft` option, to include draft mutators from composite mutators. ([#1574](https://github.com/diffplug/spotless/pull/1574)) +* `npm`-based formatters (`prettier`, `tsfmt` and `eslint`) now support caching of `node_modules` directory. + To enable it, provide the `` option. ([#1590](https://github.com/diffplug/spotless/pull/1590)) +### Fixed +* `` can now handle `Array` as a root element. ([#1585](https://github.com/diffplug/spotless/pull/1585)) +* Reduce logging-noise created by `npm`-based formatters ([#1590](https://github.com/diffplug/spotless/pull/1590) fixes [#1582](https://github.com/diffplug/spotless/issues/1582)) +### Changed +* Bump default `cleanthat` version to latest `2.1` -> `2.6` ([#1569](https://github.com/diffplug/spotless/pull/1569) and [#1574](https://github.com/diffplug/spotless/pull/1574)) + +## [2.33.0] - 2023-02-10 +### Added +* CleanThat Java Refactorer. ([#1560](https://github.com/diffplug/spotless/pull/1560)) +### Fixed +* Allow multiple instances of the same npm-based formatter to be used simultaneously. E.g. use prettier for typescript + *and* Java (using the community prettier-plugin-java) without messing up their respective `node_module` dependencies. ([#1565](https://github.com/diffplug/spotless/pull/1565)) +* `ktfmt` default style uses correct continuation indent. ([#1562](https://github.com/diffplug/spotless/pull/1562)) +### Changed +* Bump default `ktfmt` version to latest `0.42` -> `0.43` ([#1561](https://github.com/diffplug/spotless/pull/1561)) +* Bump default `jackson` version to latest `2.14.1` -> `2.14.2` ([#1536](https://github.com/diffplug/spotless/pull/1536)) + +## [2.32.0] - 2023-02-05 +### Added +* A synthesis log with the number of considered files is added after each formatter execution ([#1507](https://github.com/diffplug/spotless/pull/1507)) +### Fixed +* Respect `sourceDirectory` and `testSourceDirectory` POM configurations for Java formatters ([#1553](https://github.com/diffplug/spotless/pull/1553)) +* **POTENTIALLY BREAKING** `sortByKeys` for JSON formatting now takes into account objects inside arrays ([#1546](https://github.com/diffplug/spotless/pull/1546)) +* Any commit of the Spotless Maven plugin now available via JitPack ([#1547](https://github.com/diffplug/spotless/pull/1547)) + +## [2.31.0] - 2023-01-26 +### Added +* Prettier will now suggest to install plugins if a parser cannot be inferred from the file extension ([#1511](https://github.com/diffplug/spotless/pull/1511)) +* Jackson (`json` and `yaml`) has new `spaceBeforeSeparator` option + * **POTENTIALLY BREAKING** `spaceBeforeSeparator` is defaulted to false while the formatter was behaving with `true` +* Introduce `` ([#1492](https://github.com/diffplug/spotless/pull/1492)) + * **POTENTIALLY BREAKING** `JacksonYaml` is now configured with a `Map` to configure features +* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500)) +### Fixed +* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494) +* **POTENTIALLY BREAKING** Generate the correct qualifiedRuleId for Ktlint 0.48.x [#1495](https://github.com/diffplug/spotless/pull/1495) +### Changed +* **POTENTIALLY BREAKING** Bump minimum JRE from 8 to 11 ([#1514](https://github.com/diffplug/spotless/pull/1514) part 1 of [#1337](https://github.com/diffplug/spotless/issues/1337)) + * You can bump your build JRE without bumping your requirements ([docs](https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-source-and-target.html)). +* Spotless' custom build was replaced by [`maven-plugin-development`](https://github.com/britter/maven-plugin-development). ([#1496](https://github.com/diffplug/spotless/pull/1496) fixes [#554](https://github.com/diffplug/spotless/issues/554)) +* **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475)) + * `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019. + * From now on, we will support no more than 2 breaking changes at a time. +* `npm`-based formatters `ESLint`, `prettier` and `tsfmt` delay their `npm install` call until the formatters are first used. ([#1522](https://github.com/diffplug/spotless/pull/1522) +* Bump default `ktlint` version to latest `0.48.1` -> `0.48.2` ([#1529](https://github.com/diffplug/spotless/pull/1529)) +* Bump default `scalafmt` version to latest `3.6.1` -> `3.7.1` ([#1529](https://github.com/diffplug/spotless/pull/1529)) + +## [2.30.0] - 2023-01-13 +### Added +* Add option `editorConfigFile` for `ktLint` [#142](https://github.com/diffplug/spotless/issues/142) + * **POTENTIALLY BREAKING** `ktlint` step now modifies license headers. Make sure to put `licenseHeader` *after* `ktlint`. +* Added `skipLinesMatching` option to `licenseHeader` to support formats where license header cannot be immediately added to the top of the file (e.g. xml, sh). ([#1441](https://github.com/diffplug/spotless/pull/1441)) +* Add JSON support ([#1446](https://github.com/diffplug/spotless/pull/1446)) +* Add YAML support through Jackson ([#1478](https://github.com/diffplug/spotless/pull/1478)) +* Added support for npm-based [ESLint](https://eslint.org/)-formatter for javascript and typescript ([#1453](https://github.com/diffplug/spotless/pull/1453)) +* Better suggested messages when user's default is set by JVM limitation. ([#995](https://github.com/diffplug/spotless/pull/995)) +### Fixed +* Support `ktlint` 0.48+ new rule disabling syntax ([#1456](https://github.com/diffplug/spotless/pull/1456)) fixes ([#1444](https://github.com/diffplug/spotless/issues/1444)) +* Fix subgroups leading catch all matcher. +### Changed +* Bump default `ktlint` version to latest `0.47.1` -> `0.48.1` ([#1456](https://github.com/diffplug/spotless/pull/1456)) +* Reduce spurious invalidations of the up-to-date index file ([#1461](https://github.com/diffplug/spotless/pull/1461)) +* Bump default version for `prettier` from `2.0.5` to `2.8.1` ([#1453](https://github.com/diffplug/spotless/pull/1453)) + +## [2.29.0] - 2023-01-02 +### Added +* Added support for M2E's incremental compilation ([#1414](https://github.com/diffplug/spotless/pull/1414) fixes [#1413](https://github.com/diffplug/spotless/issues/1413)) +### Fixed +* Improve memory usage when using git ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426)) +* Support `ktlint` 0.48+ ([#1432](https://github.com/diffplug/spotless/pull/1432)) fixes ([#1430](https://github.com/diffplug/spotless/issues/1430)) +### Changed +* Bump default `ktlint` version to latest `0.47.1` -> `0.48.0` ([#1432](https://github.com/diffplug/spotless/pull/1432)) +* Bump default `ktfmt` version to latest `0.41` -> `0.42` ([#1421](https://github.com/diffplug/spotless/pull/1421)) + +## [2.28.0] - 2022-11-24 +### Added +* `importOrder` now support groups of imports without blank lines ([#1401](https://github.com/diffplug/spotless/pull/1401)) +### Fixed +* Don't treat `@Value` as a type annotation [#1367](https://github.com/diffplug/spotless/pull/1367) +* Support `ktlint_disabled_rules` in `ktlint` 0.47.x [#1378](https://github.com/diffplug/spotless/pull/1378) +* Share git repositories across projects when using ratchet ([#1426](https://github.com/diffplug/spotless/pull/1426)) +### Changed +* Bump default `ktfmt` version to latest `0.40` -> `0.41` ([#1340](https://github.com/diffplug/spotless/pull/1340)) +* Bump default `scalafmt` version to latest `3.5.9` -> `3.6.1` ([#1373](https://github.com/diffplug/spotless/pull/1373)) +* Bump default `diktat` version to latest `1.2.3` -> `1.2.4.2` ([#1393](https://github.com/diffplug/spotless/pull/1393)) +* Bump default `palantir-java-format` version to latest `2.10` -> `2.28` ([#1393](https://github.com/diffplug/spotless/pull/1393)) + +## [2.27.2] - 2022-10-10 +### Fixed +* `replace` and `replaceRegex` steps now allow you to replace something with an empty string, previously this would generate a null pointer exception. (fixes [#1359](https://github.com/diffplug/spotless/issues/1359)) + +## [2.27.1] - 2022-09-28 +### Fixed +* `skip` config key should work again now. ([#1353](https://github.com/diffplug/spotless/pull/1353) fixes [#1227](https://github.com/diffplug/spotless/issues/1227) and [#491](https://github.com/diffplug/spotless/issues/491)) + +## [2.27.0] - 2022-09-19 +### Added +* Support for `editorConfigOverride` in `ktlint`, `plugin-maven`. ([#1335](https://github.com/diffplug/spotless/pull/1335) fixes [#1334](https://github.com/diffplug/spotless/issues/1334)) + +## [2.26.0] - 2022-09-14 +### Added +* `formatAnnotations` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) +### Changed +* Bump default `ktfmt` version to latest `0.39` -> `0.40` ([#1312](https://github.com/diffplug/spotless/pull/1312)) +* Bump default `ktlint` version to latest `0.46.1` -> `0.47.1` ([#1303](https://github.com/diffplug/spotless/pull/1303)) + * Also restored support for older versions of ktlint back to `0.31.0` + +## [2.25.0] - 2022-08-23 +### Added +* `scalafmt` integration now has a configuration option `majorScalaVersion` that allows you to configure the Scala version that gets resolved from the Maven artifact ([#1283](https://github.com/diffplug/spotless/pull/1283)) +### Changed +* Add the `ktlint` rule in error messages when `ktlint` fails to apply a fix ([#1279](https://github.com/diffplug/spotless/pull/1279)) +* Bump default `scalafmt` to latest `3.0.8` -> `3.5.9` (removed support for pre-`3.0.0`) ([#1283](https://github.com/diffplug/spotless/pull/1283)) + +## [2.24.1] - 2022-08-10 +### Fixed +* Fix Clang not knowing the filename and changing the format ([#1268](https://github.com/diffplug/spotless/pull/1268) fixes [#1267](https://github.com/diffplug/spotless/issues/1267)). +### Changed +* Bump default `diktat` version to latest `1.2.1` -> `1.2.3` ([#1266](https://github.com/diffplug/spotless/pull/1266)) + +## [2.24.0] - 2022-07-28 +### Added +* Clang and Black no longer break the build when the binary is unavailable, if they will not be run during that build ([#1257](https://github.com/diffplug/spotless/pull/1257)). +* License header support for Kotlin files without `package` or `@file` but do at least have `import` ([#1263](https://github.com/diffplug/spotless/pull/1263)). + +## [2.23.0] - 2022-06-30 +### Added +* Support for `MAC_CLASSIC` (`\r`) line ending ([#1243](https://github.com/diffplug/spotless/pull/1243) fixes [#1196](https://github.com/diffplug/spotless/issues/1196)) +### Changed +* Bump default `ktlint` version to latest `0.45.2` -> `0.46.1` ([#1239](https://github.com/diffplug/spotless/issues/1239)) + * Minimum supported version also bumped to `0.46.0` (we have abandoned strong backward compatibility for `ktlint`, from here on out Spotless will only support the most-recent breaking change). +* Bump default `diktat` version to latest `1.1.0` -> `1.2.1` ([#1246](https://github.com/diffplug/spotless/pull/1246)) + * Minimum supported version also bumped to `1.2.1` (diktat is based on ktlint and has the same backward compatibility issues). +* Bump default `ktfmt` version to latest `0.37` -> `0.39` ([#1240](https://github.com/diffplug/spotless/pull/1240)) + +## [2.22.8] - 2022-06-11 +### Fixed +* `PalantirJavaFormatStep` no longer needs the `--add-exports` calls in `MAVEN_OPTS` or `.mvn/jvm.config`. ([#1233](https://github.com/diffplug/spotless/pull/1233)) + +## [2.22.7] - 2022-06-10 +### Fixed +* (Second try) `googleJavaFormat` and `removeUnusedImports` works on JDK16+ without jvm args workaround. ([#1228](https://github.com/diffplug/spotless/pull/1228)) + * If you have a bunch of `--add-exports` calls in `MAVEN_OPTS` or `.mvn/jvm.config`, you should be able to remove them. (fixes [#834](https://github.com/diffplug/spotless/issues/834#issuecomment-817524058)) + +## [2.22.6] - 2022-06-05 +### Fixed +* `googleJavaFormat` and `removeUnusedImports` works on JDK16+ without jvm args workaround. ([#1224](https://github.com/diffplug/spotless/pull/1224)) + * If you have a bunch of `--add-exports` calls in `MAVEN_OPTS` or `.mvn/jvm.config`, you should be able to remove them. (fixes [#834](https://github.com/diffplug/spotless/issues/834#issuecomment-817524058)) + +## [2.22.5] - 2022-05-10 +### Fixed +* Update the `black` version regex to fix `19.10b0` and earlier. (fixes [#1195](https://github.com/diffplug/spotless/issues/1195), regression introduced in `2.22.2`) +### Changed +* Bump default `ktfmt` version to latest `0.36` -> `0.37`. ([#1200](https://github.com/diffplug/spotless/pull/1200)) + +## [2.22.4] - 2022-05-03 +### Changed +* Bump default `diktat` version to latest `1.0.1` -> `1.1.0`. ([#1190](https://github.com/diffplug/spotless/pull/1190)) + * Converted `diktat` integration to use a compile-only source set. (fixes [#524](https://github.com/diffplug/spotless/issues/524)) + * Use the full path to a file in `diktat` integration. (fixes [#1189](https://github.com/diffplug/spotless/issues/1189)) + +## [2.22.3] - 2022-04-27 +### Changed +* Bump default `ktfmt` version to latest `0.35` -> `0.36`. ([#1183](https://github.com/diffplug/spotless/issues/1183)) +* Bump default `google-java-format` version to latest `1.13.0` -> `1.15.0`. + * ~~This means it is no longer necessary to use the `--add-exports` workaround (fixes [#834](https://github.com/diffplug/spotless/issues/834)).~~ `--add-exports` workaround is still needed. + +## [2.22.2] - 2022-04-22 +### Fixed +* Fixed support for Python Black's new version reporting. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* Error messages for unexpected file encoding now works on Java 8. (fixes [#1081](https://github.com/diffplug/spotless/issues/1081)) +### Changed +* Bump default `black` version to latest `19.10b0` -> `22.3.0`. ([#1170](https://github.com/diffplug/spotless/issues/1170)) +* Bump default `ktfmt` version to latest `0.34` -> `0.35`. ([#1159](https://github.com/diffplug/spotless/pull/1159)) +* Bump default `ktlint` version to latest `0.43.2` -> `0.45.2`. ([#1177](https://github.com/diffplug/spotless/pull/1177)) + +## [2.22.1] - 2022-04-06 +### Fixed +* Git user config and system config also included for defaultEndings configuration. ([#540](https://github.com/diffplug/spotless/issues/540)) + +## [2.22.0] - 2022-03-28 +### Added +* Added support for setting custom parameters for Kotlin ktfmt in Maven plugin. ([#1145](https://github.com/diffplug/spotless/pull/1145)) + +## [2.21.0] - 2022-02-19 +### Added +* Magic value 'NONE' for disabling ratchet functionality ([#1134](https://github.com/diffplug/spotless/issues/1134)) + +### Changed +* Use SLF4J for logging ([#1116](https://github.com/diffplug/spotless/issues/1116)) + +## [2.20.2] - 2022-02-09 +### Changed +* Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). + +## [2.20.1] - 2022-02-01 * Bump default versions of formatters ([#1095](https://github.com/diffplug/spotless/pull/1095)). * google-java-format `1.12.0` -> `1.13.0` * ktfmt `0.29` -> `0.30` +* Added support for git property `core.autocrlf` ([#540](https://github.com/diffplug/spotless/issues/540)) ## [2.20.0] - 2022-01-13 ### Added @@ -147,7 +594,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * Update ktfmt from 0.21 to 0.24 ### Fixed -* The `` field in the maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) +* The `` field in the Maven POM is now set correctly ([#798](https://github.com/diffplug/spotless/issues/798)) ## [2.10.3] - 2021-04-21 ### Fixed @@ -262,7 +709,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `` is now more robust when parsing version string for version-dependent implementation details, fixes [#668](https://github.com/diffplug/spotless/issues/668). ## [2.0.2] - 2020-08-10 -### Changes +### Changed * Bump default ktfmt from 0.13 to 0.16 ([#642](https://github.com/diffplug/spotless/pull/648)). ### Fixed @@ -281,7 +728,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config` or `configFile` with the option `parser` or `filepath` ([#620](https://github.com/diffplug/spotless/pull/620)). * Added ANTLR4 support ([#326](https://github.com/diffplug/spotless/issues/326)). ### Removed -* **BREAKING** the default includes for `` and `` were removed, and will now generate an error if an `` is not specified. There is no well-established convention for these languages in the maven ecosystem, and the performance of the default includes is far worse than a user-provided one. If you dislike this change, please complain in [#634](https://github.com/diffplug/spotless/pull/634), it would not be a breaking change to bring the defaults back. +* **BREAKING** the default includes for `` and `` were removed, and will now generate an error if an `` is not specified. There is no well-established convention for these languages in the Maven ecosystem, and the performance of the default includes is far worse than a user-provided one. If you dislike this change, please complain in [#634](https://github.com/diffplug/spotless/pull/634), it would not be a breaking change to bring the defaults back. * **BREAKING** inside the `` block, `` has been renamed to `` to avoid any confusion with the java `` ([#636](https://github.com/diffplug/spotless/pull/636)). * **BREAKING** the long-deprecated `` and `` formats have been removed, in favor of the long-available [``](https://github.com/diffplug/spotless/tree/main/plugin-maven#eclipse-wtp) step which is available in every generic format ([#630](https://github.com/diffplug/spotless/pull/630)). * This probably doesn't affect you, but if it does, you just need to change `...` into `XML...` @@ -329,7 +776,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Added * Enable IntelliJ-compatible token `$today.year` for specifying the year in license header files. ([#542](https://github.com/diffplug/spotless/pull/542)) ### Fixed -* Fix scala and kotlin maven config documentation. +* Fix scala and kotlin Maven config documentation. * Eclipse-WTP formatter (web tools platform, not java) could encounter errors in parallel multiproject builds [#492](https://github.com/diffplug/spotless/issues/492). Fixed for Eclipse-WTP formatter Eclipse version 4.13.0 (default version). ## [1.27.0] - 2020-01-01 @@ -377,11 +824,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * Updated default eclipse-jdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * Updated default eclipse-cdt from 4.11.0 to 4.12.0 ([#423](https://github.com/diffplug/spotless/pull/423)). * **KNOWN BUG - accidentally published CDT 9.7 rather than 9.8 fixed in 1.26.0** -* Added new maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) +* Added new Maven coordinates for scalafmt 2.0.0+, maintains backwards compatability ([#415](https://github.com/diffplug/spotless/issues/415)) ## [1.23.1] - 2019-06-17 * Fixes incorrect M2 cache directory path handling of Eclipse based formatters ([#401](https://github.com/diffplug/spotless/issues/401)) -* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) +* Update jgit from `4.9.0.201710071750-r` to `5.3.2.201906051522-r` because Gradle project is sometimes broken by `apache httpcomponents` in transitive dependency. ([#407](https://github.com/diffplug/spotless/pull/407)) ## [1.23.0] - 2019-04-24 * Updated default ktlint from 0.21.0 to 0.32.0, and Maven coords to com.pinterest ([#394](https://github.com/diffplug/spotless/pull/394)) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index bbb2f10792..c1a346926b 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -2,29 +2,21 @@ -[![Maven central](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) -[![Javadoc](https://img.shields.io/badge/javadoc-yes-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-maven-plugin/2.20.0/index.html) -[![Changelog](https://img.shields.io/badge/changelog-2.20.0-brightgreen.svg)](CHANGES.md) - -[![Circle CI](https://circleci.com/gh/diffplug/spotless/tree/main.svg?style=shield)](https://circleci.com/gh/diffplug/spotless/tree/main) -[![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless) -[![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) +[![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-2.44.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/2.44.4/index.html) -Spotless is a general-purpose formatting plugin. 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. +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. To people who use your build, it looks like this: @@ -47,20 +39,28 @@ user@machine repo % mvn spotless:check - [Requirements](#requirements) - [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)) + - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) - - [C/C++](#cc) ([eclipse cdt](#eclipse-cdt)) + - [C/C++](#cc) ([eclipse cdt](#eclipse-cdt), [clang-format](#clang-format)) - [Python](#python) ([black](#black)) - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) - [Sql](#sql) ([dbeaver](#dbeaver)) - [Maven Pom](#maven-pom) ([sortPom](#sortpom)) - [Markdown](#markdown) ([flexmark](#flexmark)) - - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier)) + - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Biome](#biome)) + - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Biome](#biome)) + - [JSON](#json) ([simple](#simple), [gson](#gson), [jackson](#jackson), [Biome](#biome), [jsonPatch](#jsonPatch)) + - [YAML](#yaml) + - [Gherkin](#gherkin) + - [Go](#go) + - [RDF](#RDF) + - [Protobuf](#protobuf) ([buf](#buf), [clang-format](#clang)) - Multiple languages - - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection)) + - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection), [caching `npm install` results](#caching-results-of-npm-install)) - [eclipse web tools platform](#eclipse-web-tools-platform) + - [Biome](#biome) ([binary detection](#biome-binary), [config file](#biome-configuration-file), [input language](#biome-input-language)) - **Language independent** - [Generic steps](#generic-steps) - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) @@ -70,13 +70,13 @@ user@machine repo % mvn spotless:check - [Disabling warnings and error messages](#disabling-warnings-and-error-messages) - [How do I preview what `mvn spotless:apply` will do?](#how-do-i-preview-what-mvn-spotlessapply-will-do) - [Can I apply Spotless to specific files?](#can-i-apply-spotless-to-specific-files) - - [Example configurations (from real-world projects)](#examples) + - [Example configurations (from real-world projects)](#example-configurations-from-real-world-projects) ***Contributions are welcome, see [the contributing guide](../CONTRIBUTING.md) for development info.*** ## Quickstart -To use it in your pom, just [add the Spotless dependency](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-maven-plugin%22), and configure it like so: +To use it in your pom, just [add the Spotless plugin](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-maven-plugin%22), and configure it like so: ```xml @@ -91,7 +91,7 @@ To use it in your pom, just [add the Spotless dependency](https://search.maven.o - *.md + .gitattributes .gitignore @@ -112,6 +112,7 @@ To use it in your pom, just [add the Spotless dependency](https://search.maven.o 1.8 true + false + + + + + + false - java,javax,org,com,com.diffplug,,\\#com.diffplug,\\# - + java|javax,org,com,com.diffplug,,\#com.diffplug,\# + + false + + com.example.MyPackage + + + com.example.myClass + - - - + /* (C)$YEAR */ @@ -198,63 +220,150 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` +### removeUnusedImports + +```xml + + google-java-format + +``` + ### 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). ```xml - 1.8 + 1.8 - true + true + false com.google.googlejavaformat:google-java-format ``` -**âš ī¸ Note on using Google Java Format with Java 16+** - -Using Java 16+ with Google Java Format 1.10.0 [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) to the running JDK. -These Flags can be provided using `MAVEN_OPTS` environment variable or using the `./mvn/jvm.config` file (See [documentation](https://maven.apache.org/configure.html#mvn-jvm-config-file)). - -For example the following file under `.mvn/jvm.config` will run maven with the required flags: -``` ---add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). - ### 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). ```xml - 2.10.0 + 2.39.0 + + false ``` -**âš ī¸ Note on using Palantir Java Format with Java 16+** +### eclipse jdt -Using Java 16+ with Palantir Java Format [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) on the running JDK. -These Flags can be provided using `MAVEN_OPTS` environment variable or using the `./mvn/jvm.config` file (See [documentation](https://maven.apache.org/configure.html#mvn-jvm-config-file)). +[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. -For example the following file under `.mvn/jvm.config` will run maven with the required flags: -``` ---add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +```xml + + + 4.26 + + ${project.basedir}/eclipse-formatter.xml + + + + https://download.eclipse.org/eclipse/updates/4.26/ + https://some.internal.mirror/4-26-updates-p2/ + + + ``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). -### eclipse jdt +#### Sort Members -[homepage](https://www.eclipse.org/downloads/packages/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter). [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. +Not only can you format your code with Eclipse JDT, but you can also sort the members as you know it from Eclipse IDE. +This ensures that the methods are always in sorted order (and thus reduces the likelihood of collisions in a version +control system). It is turned off by default, but you might want to consider enabling it when setting coding standards +for a project. + +The format to specify the sort order follows the `outlinesortoption` and `org.eclipse.jdt.ui.visibility.order` +properties that can be found in the workspace folder of your Eclipse IDE. You can look at the +file `.plugins/org.eclipse.core.runtime/.settings/org.eclipse.jdt.ui.prefs` in your workspace directory. ```xml - 4.13.0 - ${project.basedir}/eclipse-formatter.xml + + true + + SF,SI,SM,F,I,C,M,T + + false + + true + + B,R,D,V ``` +You can enable/disable the globally defined sort properties on file level by adding the following comments: +- `// @SortMembers:enabled=false` - disable the Sort Members feature for this file +- `// @SortMembers:doNotSortFields=true` - disable the sorting of static and instance fields +- `// @SortMembers:sortByVisibility=false` - don't sort members by its visibility modifier + +### formatAnnotations + +Type annotations should be on the same line as the type that they qualify. + +```java + @Override + @Deprecated + @Nullable @Interned String s; +``` + +However, some tools format them incorrectly, like this: + +```java + @Override + @Deprecated + @Nullable + @Interned + String s; +``` + +To fix the incorrect formatting, add the `formatAnnotations` rule after a Java formatter. For example: + +```XML + + +``` + +This does not re-order annotations, it just removes incorrect newlines. + +A type annotation is an annotation that is meta-annotated with `@Target({ElementType.TYPE_USE})`. +Because Spotless cannot necessarily examine the annotation definition, it uses a hard-coded +list of well-known type annotations. You can make a pull request to add new ones. +In the future there will be mechanisms to add/remove annotations from the list. +These mechanisms already exist for the Gradle plugin. + +### Cleanthat + +[homepage](https://github.com/solven-eu/cleanthat). CleanThat enables automatic refactoring of Java code. [ChangeLog](https://github.com/solven-eu/cleanthat/blob/master/CHANGES.MD) + +```xml + + 2.8 + ${maven.compiler.source} + + SafeAndConsensual + + + LiteralsFirstInComparisons + + + OptionalNotEmpty + + false + +``` + ## Groovy [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy). @@ -268,18 +377,20 @@ This is a workaround to a [pending issue](https://github.com/diffplug/spotless/i src/test/groovy/**/*.groovy - + - java,javax,org,com,com.diffplug,,\\#com.diffplug,\\# - + java|javax,org,com,com.diffplug,,\#com.diffplug,\# + + + + - - + /* (C)$YEAR */ - + ``` @@ -289,7 +400,7 @@ This is a workaround to a [pending issue](https://github.com/diffplug/spotless/i ```xml - 4.13.0 + 4.26 ${project.basedir}/greclipse.properties ``` @@ -325,12 +436,17 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ### ktfmt -[homepage](https://github.com/facebookincubator/ktfmt). [changelog](https://github.com/facebookincubator/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/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). ```xml - 0.30 - + 0.51 + + 120 + 4 + 8 + false + true ``` @@ -338,11 +454,26 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ### ktlint -[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java). Spotless does not ([yet](https://github.com/diffplug/spotless/issues/142)) respect the `.editorconfig` settings. +[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). +[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java). + +Spotless respects the `.editorconfig` settings by providing `editorConfigPath` option. +([ktlint docs](https://github.com/pinterest/ktlint#editorconfig)). + +Additionally, `editorConfigOverride` options will override what's supplied in `.editorconfig` file. ```xml - 0.43.2 + 1.0.0 + + true + true + + intellij_idea + + + io.nlopez.compose.rules:ktlint:0.4.16 + ``` @@ -393,8 +524,9 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ```xml - 2.0.1 + 3.5.9 ${project.basedir}/scalafmt.conf + 2.13 ``` @@ -410,7 +542,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T - src/native/** + src/native/** @@ -428,11 +560,23 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ```xml - 4.13.0 + 11.0 ${project.basedir}/eclipse-cdt.xml ``` +### clang-format + +[homepage](https://clang.llvm.org/docs/ClangFormat.html). [changelog](https://releases.llvm.org/download.html). `clang-format` is a formatter for c, c++, c#, objective-c, protobuf, javascript, and java. You can use clang-format in any language-specific format, but usually you will be creating a generic format. + +```xml + + 14.0.0-1ubuntu1.1 + /path/to/buf + + +``` + ## Python [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Python.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Black.java). @@ -481,7 +625,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T src/*/antlr4/**/*.g4 - + /* (C)$YEAR */ @@ -490,14 +634,14 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ``` -### antlr4formatter +### antlr4Formatter [homepage](https://github.com/antlr/Antlr4Formatter). [available versions](https://search.maven.org/artifact/com.khubla.antlr4formatter/antlr4-formatter). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/antlr4/Antlr4Formatter.java). ```xml - + 1.2.1 - + ``` ## SQL @@ -524,7 +668,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ```xml - dbeaver.props + dbeaver.properties ``` @@ -572,25 +716,31 @@ All configuration settings are optional, they are described in detail [here](htt ${line.separator} - true + true - false + false true + true + 2 false - false + false + + - recommended_2008_06 + recommended_2008_06 - + + + - + @@ -639,6 +789,8 @@ Currently, none of the available options can be configured yet. It uses only the + + /* (C)$YEAR */ @@ -677,10 +829,463 @@ The auto-discovery of config files (up the file tree) will not work when using t **Prerequisite: tsfmt requires a working NodeJS version** -For details, see the [npm detection](#npm-detection) and [`.npmrc` detection](#npmrc-detection) sections of prettier, which apply also to tsfmt. +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to tsfmt. + +### ESLint (typescript) + +[npm](https://www.npmjs.com/package/eslint). [changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md). *Please note:* +The auto-discovery of config files (up the file tree) will not work when using ESLint within spotless, +hence you are required to provide resolvable file paths for config files, or alternatively provide the configuration inline. + +The configuration is very similar to the [ESLint (Javascript)](#eslint-javascript) configuration. In typescript, a +reference to a `tsconfig.json` is required. + +```xml + + + 8.30.0 + + 8.30.0 + 1.2.1 + + + + eslint + 8.30.0 + + + @eslint/my-plugin-typescript + 0.14.2 + + + + ${project.basedir}/.eslintrc.js + + { + env: { + browser: true, + es2021: true + }, + extends: 'standard-with-typescript', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', + }, + rules: { + } + } + + + ${project.basedir}/tsconfig.json + +``` + +**Prerequisite: ESLint requires a working NodeJS version** + +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to ESLint. + + +## Javascript + +[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript). + +```xml + + + + src/**/*.js + + + + + + + + /* (C)$YEAR */ + REGEX_TO_DEFINE_TOP_OF_FILE + + + +``` + +### ESLint (Javascript) + +[npm](https://www.npmjs.com/package/eslint). [changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md). *Please note:* +The auto-discovery of config files (up the file tree) will not work when using ESLint within spotless, +hence you are required to provide resolvable file paths for config files, or alternatively provide the configuration inline. + +The configuration is very similar to the [ESLint (Typescript)](#eslint-typescript) configuration. In javascript, *no* +`tsconfig.json` is supported. + +```xml + + + 8.30.0 + + 8.30.0 + 1.2.1 + + + + eslint + 8.30.0 + + + @eslint/my-plugin-javascript + 0.14.2 + + + + ${project.basedir}/.eslintrc.js + + { + env: { + browser: true, + es2021: true + }, + extends: 'standard', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + } + } + + +``` + +**Prerequisite: ESLint requires a working NodeJS version** + +For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection) and [caching results of `npm install`](#caching-results-of-npm-install) sections of prettier, which apply also to ESLint. + +## JSON + +- `com.diffplug.spotless.maven.json.Json` [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java) + +```xml + + + + src/**/*.json + + + + + + + + + +``` + +### simple + +Uses a JSON pretty-printer that optionally allows configuring the number of spaces that are used to pretty print objects: + +```xml + + 4 + +``` + +### Gson + +Uses Google Gson to also allow sorting by keys besides custom indentation - useful for i18n files. + +```xml + + 4 + false + false + 2.8.1 + +``` + +Notes: +* There's no option in Gson to leave HTML as-is (i.e. escaped HTML would remain escaped, raw would remain raw). Either +all HTML characters are written escaped or none. Set `escapeHtml` if you prefer the former. +* `sortByKeys` will apply lexicographic order on the keys of the input JSON. See the +[javadoc of String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#compareTo(java.lang.String)) +for details. + +### Jackson + +Uses Jackson for formatting. + +```xml + + 2.14.1 + + true + false + true|false + + + false + true|false + + false + +``` +### jsonPatch + +Uses [zjsonpatch](https://github.com/flipkart-incubator/zjsonpatch) to apply [JSON Patches](https://jsonpatch.com/) as per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) to JSON documents. + +This enables you to add, replace or remove properties at locations in the JSON document that you specify using [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901/). + +For example, to apply the patch from the [JSON Patch homepage](https://jsonpatch.com/#the-patch): + +```xml +[ + { "op": "replace", "path": "/baz", "value": "boo" }, + { "op": "add", "path": "/hello", "value": ["world"] }, + { "op": "remove", "path": "/foo" } +] +``` + +## YAML + +- `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java) + +```xml + + + + src/**/*.yaml + + + + + + +``` + +### jackson + +Uses Jackson and YAMLFactory to pretty print objects: + +```xml + + 2.14.1 + + true + false + true|false + + + true + false + true|false + + false + +``` + +## Shell + +- `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](./src/main/java/com/diffplug/spotless/maven/shell/Shell.java) + +```xml + + + + scripts/**/*.sh + + + + + +``` + +### shfmt + +[homepage](https://github.com/mvdan/sh). [changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md). + +When formatting shell scripts via `shfmt`, configure `shfmt` settings via `.editorconfig`. + +```xml + + 3.8.0 + /opt/homebrew/bin/shfmt + +``` + +## Gherkin + +- `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/Gherkin.java) + +```xml + + + + src/**/*.feature + + + + + +``` + +### gherkinUtils + +[homepage](https://github.com/cucumber/gherkin-utils). [changelog](https://github.com/cucumber/gherkin-utils/blob/main/CHANGELOG.md). + +Uses a Gherkin pretty-printer that optionally allows configuring the number of spaces that are used to pretty print objects: + +```xml + + 9.0.0 + +``` + +## Go + +- `com.diffplug.spotless.maven.FormatterFactory.addStepFactory(FormatterStepFactory)` [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Go.java) + +```xml + + + + src/**/*.go + + + + + +``` + +### gofmt + +Standard Go formatter, part of Go distribution. + +```xml + + go1.25.1 + /opt/sdks/go1.25.1/bin/go + +``` + +## RDF + +### Generic Options + +List of generic configuration `parameters (type/default)` + +* `failOnWarning (boolean/true)`: The Jena parser produces three levels of problem reports: warning, error, and fatal. By 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). + +### 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)) +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 `` + +### Examples +Minimal: +```xml + + + + **/*.ttl + + + + +``` +Configuring some generic and TTL options: +```xml + + + + **/*.ttl + + + false + false + 1.2.13 + + RIGHT + true + + + + +``` +### 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). + +## Protobuf + +[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Protobuf.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf). +```xml + + + proto/*.proto + + + + target/**/ + + + + + + +``` + +### buf + +[homepage](https://buf.build/) [buf repo](https://github.com/bufbuild/buf). +```xml + + 1.44.0 + /path/to/buf + +``` + + +## CSS + +[code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/Css.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/css). + +```xml + + + + + src/main/css/**/*.css + src/test/css/**/*.css + + + + + + /* (C)$YEAR */ + + + +``` + +Note regarding biome: Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). + ## Prettier [homepage](https://prettier.io/). [changelog](https://github.com/prettier/prettier/blob/master/CHANGELOG.md). [official plugins](https://prettier.io/docs/en/plugins.html#official-plugins). [community plugins](https://prettier.io/docs/en/plugins.html#community-plugins). Prettier is a formatter that can format almost every anything - JavaScript, JSX, Angular, Vue, Flow, TypeScript, CSS, Less, SCSS, HTML, JSON, GraphQL, Markdown (including GFM and MDX), and YAML. It can format even more [using plugins](https://prettier.io/docs/en/plugins.html) (PHP, Ruby, Swift, XML, Apex, Elm, Java (!!), Kotlin, pgSQL, .properties, solidity, svelte, toml, shellscript, ...). @@ -704,17 +1309,19 @@ You can use prettier in any language-specific format, but usually you will be cr prettier - 2.0.5 + 2.8.8 @prettier/plugin-php - 0.14.2 + 0.19.6 ${project.basedir}/path/to/configfile true + + @prettier/plugin-php @@ -754,12 +1361,13 @@ Since spotless uses the actual npm prettier package behind the scenes, it is pos - 2.0.5 - 0.8.0 + 2.8.8 + 2.2.0 4 java + prettier-plugin-java @@ -775,16 +1383,17 @@ Since spotless uses the actual npm prettier package behind the scenes, it is pos prettier - 2.0.5 + 2.8.8 @prettier/plugin-php - 0.14.2 + 0.19.6 3 php + @prettier/plugin-php @@ -796,28 +1405,49 @@ Since spotless uses the actual npm prettier package behind the scenes, it is pos ### npm detection Prettier is based on NodeJS, so to use it, a working NodeJS installation (especially npm) is required on the host running spotless. -Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use. +Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm +and/or node binary to use. ```xml /usr/bin/npm + /usr/bin/node ``` +If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the +two, spotless will assume the other one is in the same directory. + ### `.npmrc` detection Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home. -Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with `npmExecutable`, of course.) +Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with +`npmExecutable` and `nodeExecutable`, of course.) ```xml /usr/local/shared/.npmrc ``` +### Caching results of `npm install` + +Spotless uses `npm` behind the scenes to install `prettier`. This can be a slow process, especially if you are using a slow internet connection or +if you need large plugins. You can instruct spotless to cache the results of the `npm install` calls, so that for the next installation, +it will not need to download the packages again, but instead reuse the cached version. + +```xml + + true + /usr/local/shared/.spotless-npm-install-cache +``` + +Depending on your filesystem and the location of the cache directory, spotless will use hardlinks when caching the npm packages. If that is not +possible, it will fall back to copying the files. + ## Eclipse web tools platform -[changelog](https://www.eclipse.org/webtools/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_wtp_formatters). +[changelog](https://www.eclipse.org/webtools/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_wtp_formatter). ```xml @@ -834,7 +1464,7 @@ Alternatively you can supply spotless with a location of the `.npmrc` file to us ${project.basedir}/xml.prefs ${project.basedir}/additional.properties - 4.13.0 + 4.21.0 @@ -868,6 +1498,167 @@ to true. +## Biome + +[homepage](https://biomejs.dev/). [changelog](https://github.com/biomejs/biome/blob/main/CHANGELOG.md). Biome is +a formatter that for the frontend written in Rust, which has a native binary, does not require Node.js and as such, +is pretty fast. It can currently format JavaScript, TypeScript, JSX, and JSON, and may support +[more frontend languages](https://biomejs.dev/internals/language-support/) such as CSS in the future. + +You can use Biome in any language-specific format for supported languages, but +usually you will be creating a generic format. + +Note regarding CSS: Biome supports formatting CSS as of 1.8.0 (experimental, opt-in) and 1.9.0 (stable). + +```xml + + + + + src/**/typescript/**/*.ts + + + + + 1.2.0 + + + ${project.basedir}/path/to/config/dir + + + + ts + + + + + +``` + +To apply Biome to more kinds of files with a different configuration, just add +more formats: + +```xml + + + src/**/*.ts + src/**/*.js + +``` + +**Limitations:** +- The auto-discovery of config files (up the file tree) will not work when using + Biome within spotless. +- The `ignore` option of the `biome.json` configuration file will not be applied. + Include and exclude patterns are configured in the spotless configuration in the + Maven pom instead. + +Note: Due to a limitation of biome, if the name of a file matches a pattern in +the `ignore` option of the specified `biome.json` configuration file, it will not be +formatted, even if included in the biome configuration section of the Maven pom. +You could specify a different `biome.json` configuration file without an `ignore` +pattern to circumvent this. + +Note 2: Biome is hard-coded to ignore certain special files, such as `package.json` +or `tsconfig.json`. These files will never be formatted. + +### Biome binary + +To format with Biome, spotless needs to find the Biome binary. By default, +spotless downloads the binary for the given version from the network. This +should be fine in most cases, but may not work e.g. when there is not connection +to the internet. + +To download the Biome binary from the network, just specify a version: + +```xml + + 1.2.0 + +``` + +Spotless uses a default version when you do not specify a version, but this +may change at any time, so we recommend that you always set the Biome version +you want to use. Optionally, you can also specify a directory for the downloaded +Biome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/biome`): + +```xml + + 1.2.0 + + ${user.home}/biome + +``` + +To use a fixed binary, omit the `version` and specify a `pathToExe`: + +```xml + + ${project.basedir}/bin/biome + +``` + +Absolute paths are used as-is. Relative paths are resolved against the project's +base directory. To use a pre-installed Biome binary on the user's path, specify +just a name without any slashes / backslashes: + + +```xml + + + biome + +``` + +### Biome configuration file + +Biome is a biased formatter and linter without many options, but there are a few +basic options. Biome uses a file named [biome.json](https://biomejs.dev/reference/configuration/) +for its configuration. When none is specified, the default configuration from +Biome is used. To use a custom configuration: + +```xml + + + + ${project.basedir} + +``` + +### Biome input language + +By default, Biome detects the language / syntax of the files to format +automatically from the file extension. This may fail if your source code files +have unusual extensions for some reason. If you are using the generic format, +you can force a certain language like this: + +```xml + + + + + src/**/typescript/**/*.mjson + + + + 1.2.0 + json + + + + +``` + +The following languages are currently recognized: + +* `js` -- JavaScript +* `jsx` -- JavaScript + JSX (React) +* `js?` -- JavaScript, with or without JSX, depending on the file extension +* `ts` -- TypeScript +* `tsx` -- TypeScript + JSX (React) +* `ts?` -- TypeScript, with or without JSX, depending on the file extension +* `json` -- JSON + ## Generic steps [Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these: @@ -884,7 +1675,7 @@ to true. Greetings to Mars - org.codehaus.groovy:groovy-jsr223:3.0.9 + org.codehaus.groovy:groovy-jsr223:3.0.9 groovy @@ -892,7 +1683,7 @@ to true. Greetings to Mars from sed /usr/bin/sed - + s/World/Mars/g @@ -935,13 +1726,19 @@ Once a file's license header has a valid year, whether it is a year (`2020`) or If your project has not been rigorous with copyright headers, and you'd like to use git history to repair this retroactively, you can do so with `-DspotlessSetLicenseHeaderYearsFromGitHistory=true`. When run in this mode, Spotless will do an expensive search through git history for each file, and set the copyright header based on the oldest and youngest commits for that file. This is intended to be a one-off sort of thing. +### Files with fixed header lines + +Some files have fixed header lines (e.g. `^#!.+?$` to skip shebangs). + ## Incremental up-to-date checking and formatting -**This feature is turned off by default.** +**This feature is enabled by default starting from version 2.35.0.** Execution of `spotless:check` and `spotless:apply` for large projects can take time. By default, Spotless Maven plugin needs to read and format each source file. @@ -999,6 +1796,21 @@ However, we strongly recommend that you use a non-local branch, such as a tag or This is especially helpful for injecting accurate copyright dates using the [license step](#license-header). +You can explicitly disable ratchet functionality by providing the value 'NONE': +```xml + + NONE + +``` +This is useful for disabling the ratchet functionality in child projects where the parent defines a ratchetFrom value. + +### Using `ratchetFrom` on CI systems + +Many popular CI systems (GitHub, GitLab, BitBucket, and Travis) use a "shallow clone". This means that `origin/main` will fail with `No such reference`. You can fix this by: + +- calling `git fetch origin main` before you call Spotless +- disabling the shallow clone [like so](https://github.com/diffplug/spotless/issues/710) + ## `spotless:off` and `spotless:on` Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this: @@ -1026,9 +1838,15 @@ Spotless uses UTF-8 by default, but you can use [any encoding which Java support ``` -Line endings can also be set globally or per-format using the `lineEndings` property. Spotless supports four line ending modes: `UNIX`, `WINDOWS`, `PLATFORM_NATIVE`, and `GIT_ATTRIBUTES`. The default value is `GIT_ATTRIBUTES`, and *we highly recommend that you* ***do not change*** *this value*. Git has opinions about line endings, and if Spotless and git disagree, then you're going to have a bad time. +Line endings can also be set globally or per-format using the `lineEndings` property. Spotless supports + +- constant modes (`UNIX`, `WINDOWS`, `MAC_CLASSIC`) +- simple modes (`PLATFORM_NATIVE`, `PRESERVE`) +- and git-aware modes (`GIT_ATTRIBUTES`, `GIT_ATTRIBUTES_FAST_ALLSAME`) -You can easily set the line endings of different files using [a `.gitattributes` file](https://help.github.com/articles/dealing-with-line-endings/). Here's an example `.gitattributes` which sets all files to unix newlines: `* text eol=lf`. +The default value is `GIT_ATTRIBUTES_FAST_ALLSAME`, and *we highly recommend that you* ***do not change*** *this value*. Git has opinions about line endings, and if Spotless and git disagree, then you're going to have a bad time. `FAST_ALLSAME` just means that Spotless can assume that every file being formatted has the same line endings ([more info](https://github.com/diffplug/spotless/pull/1838)). + +You can easily set the line endings of different files using [a `.gitattributes` file](https://help.github.com/articles/dealing-with-line-endings/). Here's an example `.gitattributes` which sets all files to unix newlines: `* text eol=lf`. @@ -1059,6 +1877,28 @@ cmd> mvn spotless:apply -DspotlessFiles=my/file/pattern.java,more/generic/.*-pat The patterns are matched using `String#matches(String)` against the absolute file path. +## Does Spotless support incremental builds in Eclipse? + +Spotless comes with [m2e](https://eclipse.dev/m2e/) support. However, by default its execution is skipped in incremental builds as most developers want to fix all issues in one go via explicit `mvn spotless:apply` prior to raising a PR and don't want to be bothered with Spotless issues during working on the source code in the IDE. +To enable it use the following parameter + +``` + + true + +``` + +In addition Eclipse problem markers are being emitted for goal `check`. By default they have the severity `WARNING`. +You can adjust this with + +``` + + ERROR + +``` + +Note that for Incremental build support the goals have to be bound to a phase prior to `test`. + ## Example configurations (from real-world projects) diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle index 0905f58c24..7932d4a007 100644 --- a/plugin-maven/build.gradle +++ b/plugin-maven/build.gradle @@ -1,73 +1,30 @@ -buildscript { - repositories { mavenCentral() } - dependencies { classpath "com.github.spullara.mustache.java:compiler:${VER_MUSTACHE}" } -} -plugins { - id 'cz.malohlava.visteg' version '1.0.5' // https://github.com/mmalohlava/gradle-visteg -} -apply from: rootProject.file('gradle/changelog.gradle') -apply from: rootProject.file('gradle/spotless-freshmark.gradle') +import org.gradlex.maven.plugin.development.task.GenerateHelpMojoSourcesTask +import org.gradlex.maven.plugin.development.task.GenerateMavenPluginDescriptorTask -// to generate taskGraph.pdf -// - set enabled (below) to true -// - run: ./gradlew :plugin-maven:test -// - run: rm plugin-maven/output.pdf -// - run: dot -Tpdf plugin-maven/build/reports/visteg.dot > plugin-maven/taskGraph.pdf -visteg { - enabled = false - nodeShape = 'box' - startNodeShape = 'box' - endNodeShape = 'box' - colorscheme = 'pastel24' // https://www.graphviz.org/doc/info/colors.html +plugins { + // https://github.com/gradlex-org/maven-plugin-development + id 'org.gradlex.maven-plugin-development' version '1.0.3' } -import com.github.mustachejava.DefaultMustacheFactory - -import java.nio.file.Files -import java.nio.file.Paths - -import static java.nio.charset.StandardCharsets.UTF_8 -import static java.nio.file.StandardOpenOption.CREATE_NEW -import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING - +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/java-publish.gradle") -final PROJECT_DIR = project.projectDir.toString() -final BUILD_DIR = project.buildDir.toString() -final MAVEN_PROJECT_DIR = "${BUILD_DIR}/mavenProject" -final LOCAL_MAVEN_REPO_DIR = "${BUILD_DIR}/localMavenRepository" +apply from: rootProject.file("gradle/java-setup.gradle") +apply from: rootProject.file('gradle/spotless-freshmark.gradle') -def mvnw(String args) { - boolean isWin = System.getProperty('os.name').toLowerCase().contains('win') - if (isWin) { - return [ - 'cmd', - '/c', - 'mvnw.cmd -e ' + args - ] - } else { - return [ - '/bin/sh', - '-c', - './mvnw -e ' + args - ] - } +mavenPlugin { + name = 'Spotless Maven Plugin' + artifactId = project.artifactIdMaven + description = project.description } -String libVersion = version.endsWith('-SNAPSHOT') ? - rootProject.spotlessChangelog.versionNext : - rootProject.spotlessChangelog.versionLast +String VER_MAVEN_API = '3.0' +String VER_ECLIPSE_AETHER = '1.1.0' +String VER_PLEXUS_RESOURCES = '1.3.0' dependencies { - if (version.endsWith('-SNAPSHOT') || (rootProject.spotlessChangelog.versionNext == rootProject.spotlessChangelog.versionLast)) { - implementation project(':lib') - implementation project(':lib-extra') - } else { - implementation "com.diffplug.spotless:spotless-lib:${libVersion}" - implementation "com.diffplug.spotless:spotless-lib-extra:${libVersion}" - } + implementation projects.lib + implementation projects.libExtra compileOnly "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}" compileOnly "org.apache.maven.plugin-tools:maven-plugin-annotations:${VER_MAVEN_API}" @@ -75,127 +32,32 @@ dependencies { compileOnly "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}" implementation "com.diffplug.durian:durian-core:${VER_DURIAN}" + implementation "com.diffplug.durian:durian-io:${VER_DURIAN}" implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" implementation("org.codehaus.plexus:plexus-resources:${VER_PLEXUS_RESOURCES}") implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}" + implementation 'org.sonatype.plexus:plexus-build-api:0.0.7' testImplementation project(":testlib") testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "org.mockito:mockito-core:${VER_MOCKITO}" testImplementation "com.diffplug.durian:durian-io:${VER_DURIAN}" - testImplementation "com.github.spullara.mustache.java:compiler:${VER_MUSTACHE}" + testImplementation 'com.github.spullara.mustache.java:compiler:0.9.14' + testImplementation 'org.owasp.encoder:encoder:1.3.1' testImplementation "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}" testImplementation "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}" testImplementation "org.codehaus.plexus:plexus-resources:${VER_PLEXUS_RESOURCES}" testImplementation "org.apache.maven:maven-core:${VER_MAVEN_API}" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" } -task cleanMavenProjectDir(type: Delete) { delete MAVEN_PROJECT_DIR } - -task copySourceFiles(type: Sync, dependsOn: cleanMavenProjectDir) { - from "src/main/java" - into "${MAVEN_PROJECT_DIR}/src/main/java" -} - -task copyMvnw(type: Copy, dependsOn: copySourceFiles) { - from 'src/test/resources' - include 'mvnw' - include 'mvnw.cmd' - include '.mvn/**' - into MAVEN_PROJECT_DIR -} - -task installLocalDependencies -def libs = [ - 'lib', - 'lib-extra', - 'testlib' -] -libs.each { - def groupId = 'com.diffplug.spotless' - def artifactId = "spotless-${it}" - def jarTask = tasks.getByPath(":${it}:jar") - def file = jarTask.archivePath - - def installDependency = task "install_${artifactId}"(type: Exec) { - workingDir MAVEN_PROJECT_DIR - - inputs.file(file) - outputs.dir(project.file("${LOCAL_MAVEN_REPO_DIR}/${groupId.replace('.', '/')}/${artifactId}/${version}")) - commandLine mvnw("org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file " + - "-Dfile=${file} " + - "-DgroupId=${groupId} " + - "-DartifactId=${artifactId} " + - "-Dversion=${libVersion} " + - "-Dpackaging=jar " + - "-DlocalRepositoryPath=${LOCAL_MAVEN_REPO_DIR}") - } - installDependency.dependsOn(jarTask) - - installLocalDependencies.dependsOn installDependency -} - -task createPomXml(dependsOn: installLocalDependencies) { - doLast { - def additionalDependencies = project.configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.findAll { - return !libs.contains(it.moduleVersion.id.name) - }.collect { - return " \n" + - " ${it.moduleVersion.id.group}\n" + - " ${it.moduleVersion.id.name}\n" + - " ${it.moduleVersion.id.version}\n" + - " \n" - }.join() - - def versions = [ - spotlessMavenPluginVersion: version, - mavenApiVersion : VER_MAVEN_API, - eclipseAetherVersion : VER_ECLIPSE_AETHER, - spotlessLibVersion : libVersion, - jsr305Version : VER_JSR_305, - additionalDependencies : additionalDependencies - ] - - def pomXmlTemplate = Paths.get(PROJECT_DIR, "src/test/resources/pom-build.xml.mustache") - def newPomXml = Paths.get(MAVEN_PROJECT_DIR, "pom.xml") - - Files.newBufferedReader(pomXmlTemplate).withCloseable { reader -> - Files.newBufferedWriter(newPomXml, UTF_8, CREATE_NEW, TRUNCATE_EXISTING).withCloseable { writer -> - def mustache = new DefaultMustacheFactory().compile(reader, "pom") - mustache.execute(writer, versions) - } - } - } -} - -task runMavenBuild(type: Exec, dependsOn: [ - cleanMavenProjectDir, - copySourceFiles, - copyMvnw, - createPomXml -]) { - workingDir MAVEN_PROJECT_DIR - // -B batch mode to make dependency download logging less verbose - commandLine mvnw("clean install -B -Dmaven.repo.local=${LOCAL_MAVEN_REPO_DIR}") -} - -jar.setActions Arrays.asList() -jar.dependsOn(runMavenBuild) -File jarIn = file("${MAVEN_PROJECT_DIR}/target/spotless-maven-plugin-${version}.jar") -File jarOut = jar.archivePath -jar.inputs.file(jarIn) -jar.outputs.file(jarOut) -jar.doLast { - Files.copy(jarIn.toPath(), jarOut.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) -} - -test { useJUnitPlatform() } - apply from: rootProject.file('gradle/special-tests.gradle') - -tasks.withType(Test) { - systemProperty "localMavenRepositoryDir", LOCAL_MAVEN_REPO_DIR - systemProperty "spotlessMavenPluginVersion", project.version - dependsOn(jar) +tasks.withType(Test).configureEach { + systemProperty 'spotlessMavenPluginVersion', project.version + dependsOn 'publishToMavenLocal' + dependsOn ':lib:publishToMavenLocal' + dependsOn ':lib-extra:publishToMavenLocal' } + +apply from: rootProject.file("gradle/java-publish.gradle") 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 1b5abe9f0c..d21a1b8113 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -50,31 +49,46 @@ import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; +import org.sonatype.plexus.build.incremental.BuildContext; import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.Jvm; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.maven.antlr4.Antlr4; import com.diffplug.spotless.maven.cpp.Cpp; +import com.diffplug.spotless.maven.css.Css; import com.diffplug.spotless.maven.generic.Format; import com.diffplug.spotless.maven.generic.LicenseHeader; +import com.diffplug.spotless.maven.gherkin.Gherkin; +import com.diffplug.spotless.maven.go.Go; import com.diffplug.spotless.maven.groovy.Groovy; import com.diffplug.spotless.maven.incremental.UpToDateChecker; import com.diffplug.spotless.maven.incremental.UpToDateChecking; import com.diffplug.spotless.maven.java.Java; +import com.diffplug.spotless.maven.javascript.Javascript; +import com.diffplug.spotless.maven.json.Json; import com.diffplug.spotless.maven.kotlin.Kotlin; import com.diffplug.spotless.maven.markdown.Markdown; import com.diffplug.spotless.maven.pom.Pom; +import com.diffplug.spotless.maven.protobuf.Protobuf; import com.diffplug.spotless.maven.python.Python; +import com.diffplug.spotless.maven.rdf.Rdf; import com.diffplug.spotless.maven.scala.Scala; +import com.diffplug.spotless.maven.shell.Shell; import com.diffplug.spotless.maven.sql.Sql; import com.diffplug.spotless.maven.typescript.Typescript; +import com.diffplug.spotless.maven.yaml.Yaml; public abstract class AbstractSpotlessMojo extends AbstractMojo { private static final String DEFAULT_INDEX_FILE_NAME = "spotless-index"; private static final String DEFAULT_ENCODING = "UTF-8"; - private static final String DEFAULT_LINE_ENDINGS = "GIT_ATTRIBUTES"; + private static final String DEFAULT_LINE_ENDINGS = "GIT_ATTRIBUTES_FAST_ALLSAME"; + + /** Value to allow unsetting the ratchet inherited from parent pom configuration. */ + static final String RATCHETFROM_NONE = "NONE"; + static final String GOAL_CHECK = "check"; static final String GOAL_APPLY = "apply"; @@ -84,9 +98,15 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Component private ResourceManager resourceManager; + @Component + protected BuildContext buildContext; + @Parameter(defaultValue = "${mojoExecution.goal}", required = true, readonly = true) private String goal; + @Parameter(property = "spotless.skip", defaultValue = "false") + private boolean skip; + @Parameter(property = "spotless.apply.skip", defaultValue = "false") private boolean applySkip; @@ -103,7 +123,7 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { private List repositories; @Parameter(defaultValue = "${project.basedir}", required = true, readonly = true) - private File baseDir; + protected File baseDir; @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true) private File buildDir; @@ -123,6 +143,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter private List formats = Collections.emptyList(); + @Parameter + private Css css; + @Parameter private Groovy groovy; @@ -141,6 +164,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter private Typescript typescript; + @Parameter + private Javascript javascript; + @Parameter private Antlr4 antlr4; @@ -156,6 +182,27 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter private Markdown markdown; + @Parameter + private Json json; + + @Parameter + private Shell shell; + + @Parameter + private Yaml yaml; + + @Parameter + private Gherkin gherkin; + + @Parameter + private Go go; + + @Parameter + private Rdf rdf; + + @Parameter + private Protobuf protobuf; + @Parameter(property = "spotlessFiles") private String filePatterns; @@ -163,9 +210,26 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { private String setLicenseHeaderYearsFromGitHistory; @Parameter - private UpToDateChecking upToDateChecking; + private UpToDateChecking upToDateChecking = UpToDateChecking.enabled(); + + /** + * If set to {@code true} will also run on incremental builds (i.e. within Eclipse with m2e). + * Otherwise this goal is skipped in incremental builds and only runs on full builds. + */ + @Parameter(defaultValue = "false") + protected boolean m2eEnableForIncrementalBuild; - protected abstract void process(Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException; + protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException; + + private static final int MINIMUM_JRE = 11; + + protected AbstractSpotlessMojo() { + if (Jvm.version() < MINIMUM_JRE) { + throw new RuntimeException("Spotless requires JRE " + MINIMUM_JRE + " or newer, this was " + Jvm.version() + ".\n" + + "You can upgrade your build JRE and still compile for older targets, see below\n" + + "https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation"); + } + } @Override public final void execute() throws MojoExecutionException { @@ -177,18 +241,18 @@ public final void execute() throws MojoExecutionException { List formatterFactories = getFormatterFactories(); FormatterConfig config = getFormatterConfig(); - Map>> formatterFactoryToFiles = new HashMap<>(); + Map>> formatterFactoryToFiles = new LinkedHashMap<>(); for (FormatterFactory formatterFactory : formatterFactories) { Supplier> filesToFormat = () -> collectFiles(formatterFactory, config); formatterFactoryToFiles.put(formatterFactory, filesToFormat); } try (FormattersHolder formattersHolder = FormattersHolder.create(formatterFactoryToFiles, config); - UpToDateChecker upToDateChecker = createUpToDateChecker(formattersHolder.getFormatters())) { - for (Entry>> entry : formattersHolder.getFormattersWithFiles().entrySet()) { - Formatter formatter = entry.getKey(); - Iterable files = entry.getValue().get(); - process(files, formatter, upToDateChecker); + UpToDateChecker upToDateChecker = createUpToDateChecker(formattersHolder.openFormatters.values())) { + for (FormatterFactory factory : formattersHolder.openFormatters.keySet()) { + Formatter formatter = formattersHolder.openFormatters.get(factory); + Iterable files = formattersHolder.factoryToFiles.get(factory).get(); + process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker); } } catch (PluginException e) { throw e.asMojoExecutionException(); @@ -196,6 +260,14 @@ public final void execute() throws MojoExecutionException { } private boolean shouldSkip() { + if (skip) { + return true; + } + if (buildContext.isIncremental() && !m2eEnableForIncrementalBuild) { + getLog().debug("Skipping for incremental builds as parameter 'enableForIncrementalBuilds' is set to 'false'"); + return true; + } + switch (goal) { case GOAL_CHECK: return checkSkip; @@ -204,6 +276,7 @@ private boolean shouldSkip() { default: break; } + return false; } @@ -281,7 +354,7 @@ private static String withTrailingSeparator(String path) { private Set getIncludes(FormatterFactory formatterFactory) { Set configuredIncludes = formatterFactory.includes(); - Set includes = configuredIncludes.isEmpty() ? formatterFactory.defaultIncludes() : configuredIncludes; + Set includes = configuredIncludes.isEmpty() ? formatterFactory.defaultIncludes(project) : configuredIncludes; if (includes.isEmpty()) { throw new PluginException("You must specify some files to include, such as 'src/**/*.blah'"); } @@ -302,7 +375,9 @@ private FormatterConfig getFormatterConfig() { Provisioner provisioner = MavenProvisioner.create(resolver); List formatterStepFactories = getFormatterStepFactories(); FileLocator fileLocator = getFileLocator(); - return new FormatterConfig(baseDir, encoding, lineEndings, Optional.ofNullable(ratchetFrom), provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory)); + final Optional optionalRatchetFrom = Optional.ofNullable(this.ratchetFrom) + .filter(ratchet -> !RATCHETFROM_NONE.equals(ratchet)); + return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory)); } private FileLocator getFileLocator() { @@ -313,8 +388,9 @@ private FileLocator getFileLocator() { } private List getFormatterFactories() { - return Stream.concat(formats.stream(), Stream.of(groovy, java, scala, kotlin, cpp, typescript, antlr4, pom, sql, python, markdown)) + return Stream.concat(formats.stream(), Stream.of(groovy, java, scala, kotlin, cpp, css, typescript, javascript, antlr4, pom, sql, python, markdown, json, shell, yaml, gherkin, go, rdf, protobuf)) .filter(Objects::nonNull) + .map(factory -> factory.init(repositorySystemSession)) .collect(toList()); } @@ -330,10 +406,13 @@ private UpToDateChecker createUpToDateChecker(Iterable formatters) { Path targetDir = project.getBasedir().toPath().resolve(project.getBuild().getDirectory()); indexFile = targetDir.resolve(DEFAULT_INDEX_FILE_NAME); } + final UpToDateChecker checker; if (upToDateChecking != null && upToDateChecking.isEnabled()) { - getLog().info("Up-to-date checking enabled"); - return UpToDateChecker.forProject(project, indexFile, formatters, getLog()); + checker = UpToDateChecker.forProject(project, indexFile, formatters, getLog()); + } else { + getLog().info("Up-to-date checking disabled"); + checker = UpToDateChecker.noop(project, indexFile, getLog()); } - return UpToDateChecker.noop(project, indexFile, getLog()); + return UpToDateChecker.wrapWithBuildContext(checker, buildContext); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java index 7ea998dc95..088c35a3d0 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 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,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; @@ -34,17 +38,18 @@ public class FileLocator { static final String TMP_RESOURCE_FILE_PREFIX = "spotless-resource-"; private final ResourceManager resourceManager; - private final File baseDir, buildDir; + private final File baseDir, buildDir, dataDir; public FileLocator(ResourceManager resourceManager, File baseDir, File buildDir) { this.resourceManager = Objects.requireNonNull(resourceManager); this.baseDir = Objects.requireNonNull(baseDir); this.buildDir = Objects.requireNonNull(buildDir); + this.dataDir = findDataDir(); } /** - * If the given path is a local file returns it as such unchanged, - * otherwise extracts the given resource to a randomly-named file in the build folder. + * If the given path is a local file returns it as such unchanged, otherwise + * extracts the given resource to a randomly-named file in the build folder. */ public File locateFile(String path) { if (isNullOrEmpty(path)) { @@ -62,18 +67,42 @@ public File locateFile(String path) { } catch (ResourceNotFoundException e) { throw new RuntimeException("Unable to locate file with path: " + path, e); } catch (FileResourceCreationException e) { - throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory", e); + throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory", + e); } } + /** + * Finds the base directory of the Maven or Gradle project on which spotless is + * currently being executed. + * + * @return The base directory of the current Maven or Gradel project. + */ public File getBaseDir() { return baseDir; } + /** + * Finds the build directory (e.g. /target) of the Maven or Gradle + * project on which spotless is currently being executed. + * + * @return The project build directory of the current Maven or Gradle project. + */ public File getBuildDir() { return buildDir; } + /** + * Finds the data directory that can be used for storing shared data such as + * downloaded files globally. This is a directory in the local repository, e.g. + * ~/.m2/repository/com/diffplus/spotless/spotless-data. + * + * @return The directory for storing shared data. + */ + public File getDataDir() { + return dataDir; + } + private static String tmpOutputFileName(String path) { String extension = FileUtils.extension(path); byte[] pathHash = hash(path); @@ -91,4 +120,33 @@ private static byte[] hash(String value) { messageDigest.update(value.getBytes(UTF_8)); return messageDigest.digest(); } + + private static File findDataDir() { + try { + // JAR path is e.g. + // ~/.m2/repository/com/diffplug/spotless/spotless-plugin-maven/1.2.3/spotless-plugin-maven-1.2.3.jar + var codeSource = FileLocator.class.getProtectionDomain().getCodeSource(); + var location = codeSource != null ? codeSource.getLocation() : null; + var locationUri = location != null ? location.toURI() : null; + var jarPath = locationUri != null && "file".equals(locationUri.getScheme()) ? Path.of(locationUri) : null; + var parent1 = jarPath != null ? jarPath.getParent() : null; + var parent2 = parent1 != null ? parent1.getParent() : null; + var base = parent2 != null ? parent2.getParent() : null; + var sub = base != null ? base.resolve("spotless-data") : null; + if (sub != null) { + return sub.toAbsolutePath().toFile(); + } else { + return findUserHome(); + } + } catch (final SecurityException e) { + return findUserHome(); + } catch (final URISyntaxException | FileSystemNotFoundException | IllegalArgumentException e) { + throw new RuntimeException("Unable to determine data directory in local Maven repository", e); + } + } + + private static File findUserHome() { + var home = Paths.get(System.getenv("user.home")); + return home.resolve(".rome").toAbsolutePath().toFile(); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java index c06822486c..456c31b9a3 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 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,7 @@ */ package com.diffplug.spotless.maven; +import static com.diffplug.spotless.maven.AbstractSpotlessMojo.RATCHETFROM_NONE; import static java.util.Collections.emptySet; import java.io.File; @@ -28,14 +29,24 @@ import java.util.stream.Collectors; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.eclipse.aether.RepositorySystemSession; import com.diffplug.common.collect.Sets; -import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; -import com.diffplug.spotless.generic.PipeStepPair; -import com.diffplug.spotless.maven.generic.*; +import com.diffplug.spotless.maven.generic.EclipseWtp; +import com.diffplug.spotless.maven.generic.EndWithNewline; +import com.diffplug.spotless.maven.generic.Indent; +import com.diffplug.spotless.maven.generic.Jsr223; +import com.diffplug.spotless.maven.generic.LicenseHeader; +import com.diffplug.spotless.maven.generic.NativeCmd; +import com.diffplug.spotless.maven.generic.Prettier; +import com.diffplug.spotless.maven.generic.Replace; +import com.diffplug.spotless.maven.generic.ReplaceRegex; +import com.diffplug.spotless.maven.generic.ToggleOffOn; +import com.diffplug.spotless.maven.generic.TrimTrailingWhitespace; public abstract class FormatterFactory { @Parameter @@ -60,7 +71,7 @@ public abstract class FormatterFactory { private ToggleOffOn toggle; - public abstract Set defaultIncludes(); + public abstract Set defaultIncludes(MavenProject project); public abstract String licenseHeaderDelimiter(); @@ -85,17 +96,14 @@ public final Formatter newFormatter(Supplier> filesToFormat, Form .map(factory -> factory.newFormatterStep(stepConfig)) .collect(Collectors.toCollection(() -> new ArrayList())); if (toggle != null) { - PipeStepPair pair = toggle.createPair(); - formatterSteps.add(0, pair.in()); - formatterSteps.add(pair.out()); + List formatterStepsBeforeToggle = formatterSteps; + formatterSteps = List.of(toggle.createFence().preserveWithin(formatterStepsBeforeToggle)); } return Formatter.builder() .encoding(formatterEncoding) .lineEndingsPolicy(formatterLineEndingPolicy) - .exceptionPolicy(new FormatExceptionPolicyStrict()) .steps(formatterSteps) - .rootDir(config.getFileLocator().getBaseDir().toPath()) .build(); } @@ -159,6 +167,8 @@ private LineEnding lineEndings(FormatterConfig config) { Optional ratchetFrom(FormatterConfig config) { if (RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL.equals(ratchetFrom)) { return config.getRatchetFrom(); + } else if (RATCHETFROM_NONE.equals(ratchetFrom)) { + return Optional.empty(); } else { return Optional.ofNullable(ratchetFrom); } @@ -183,4 +193,9 @@ private static boolean formatterStepOverriden(FormatterStepFactory global, List< return allConfigured.stream() .anyMatch(configured -> configured.getClass() == global.getClass()); } + + public FormatterFactory init(RepositorySystemSession repositorySystemSession) { + stepFactories.forEach(factory -> factory.init(repositorySystemSession)); + return this; + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepFactory.java index 119aa56961..4642f17de5 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepFactory.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterStepFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,15 @@ */ package com.diffplug.spotless.maven; +import org.eclipse.aether.RepositorySystemSession; + import com.diffplug.spotless.FormatterStep; public interface FormatterStepFactory { FormatterStep newFormatterStep(FormatterStepConfig config); + + default void init(RepositorySystemSession repositorySystemSession) { + // nothing + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormattersHolder.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormattersHolder.java index 1c226423f6..9b279c8845 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormattersHolder.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormattersHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,62 +16,58 @@ package com.diffplug.spotless.maven; import java.io.File; -import java.util.HashMap; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.function.Supplier; import com.diffplug.spotless.Formatter; class FormattersHolder implements AutoCloseable { + final Map openFormatters; + final Map>> factoryToFiles; - private final Map>> formatterToFiles; + FormattersHolder(Map openFormatters, Map>> factoryToFiles) { + this.openFormatters = openFormatters; + this.factoryToFiles = factoryToFiles; + } - FormattersHolder(Map>> formatterToFiles) { - this.formatterToFiles = formatterToFiles; + public String nameFor(FormatterFactory factory) { + return factory.getClass().getSimpleName(); } static FormattersHolder create(Map>> formatterFactoryToFiles, FormatterConfig config) { - Map>> formatterToFiles = new HashMap<>(); + Map openFormatters = new LinkedHashMap<>(); try { for (Entry>> entry : formatterFactoryToFiles.entrySet()) { FormatterFactory formatterFactory = entry.getKey(); Supplier> files = entry.getValue(); - Formatter formatter = formatterFactory.newFormatter(files, config); - formatterToFiles.put(formatter, files); + openFormatters.put(formatterFactory, formatter); } } catch (RuntimeException openError) { try { - close(formatterToFiles.keySet()); + close(openFormatters.values()); } catch (Exception closeError) { openError.addSuppressed(closeError); } throw openError; } - return new FormattersHolder(formatterToFiles); - } - - Iterable getFormatters() { - return formatterToFiles.keySet(); - } - - Map>> getFormattersWithFiles() { - return formatterToFiles; + return new FormattersHolder(openFormatters, formatterFactoryToFiles); } @Override public void close() { try { - close(formatterToFiles.keySet()); + close(openFormatters.values()); } catch (Exception e) { throw new RuntimeException("Unable to close formatters", e); } } - private static void close(Set formatters) throws Exception { + private static void close(Collection formatters) throws Exception { Exception error = null; for (Formatter formatter : formatters) { try { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/IdeHook.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/IdeHook.java new file mode 100644 index 0000000000..6b7750c060 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/IdeHook.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2024 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; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import com.diffplug.common.base.Errors; +import com.diffplug.common.io.ByteStreams; +import com.diffplug.spotless.DirtyState; +import com.diffplug.spotless.Formatter; + +class IdeHook { + + private static void dumpIsClean() { + System.err.println("IS CLEAN"); + } + + //No need to check ratchet (using isClean()) as it is performed in Gradle's IDE hook, since we have already gathered the available git files from ratchet. + static void performHook(Iterable projectFiles, Formatter formatter, String path, boolean spotlessIdeHookUseStdIn, boolean spotlessIdeHookUseStdOut) { + File file = new File(path); + if (!file.isAbsolute()) { + System.err.println("Argument passed to spotlessIdeHook must be an absolute path"); + return; + } + + if (!projectContainsFile(projectFiles, file)) { + return; + } + + try { + byte[] bytes; + if (spotlessIdeHookUseStdIn) { + bytes = ByteStreams.toByteArray(System.in); + } else { + bytes = Files.readAllBytes(file.toPath()); + } + DirtyState dirty = DirtyState.of(formatter, file, bytes); + if (dirty.isClean()) { + dumpIsClean(); + } else if (dirty.didNotConverge()) { + System.err.println("DID NOT CONVERGE"); + System.err.println("See details https://github.com/diffplug/spotless/blob/main/PADDEDCELL.md"); + } else { + System.err.println("IS DIRTY"); + if (spotlessIdeHookUseStdOut) { + dirty.writeCanonicalTo(System.out); + } else { + dirty.writeCanonicalTo(file); + } + } + } catch (IOException e) { + e.printStackTrace(System.err); + throw Errors.asRuntime(e); + } finally { + System.err.close(); + System.out.close(); + } + } + + private static boolean projectContainsFile(Iterable projectFiles, File file) { + for (File projectFile : projectFiles) { + if (projectFile.equals(file)) { + return true; + } + } + return false; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/ImpactedFilesTracker.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/ImpactedFilesTracker.java new file mode 100644 index 0000000000..60eb8af762 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/ImpactedFilesTracker.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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; + +/** + * Tracks the number of processed files, typically by a single Formatter for a whole repository + */ +class ImpactedFilesTracker { + protected int nbskippedAsCleanCache = 0; + protected int nbCheckedButAlreadyClean = 0; + protected int nbCleaned = 0; + + /** + * Some cache mechanism may indicate some content is clean, without having to execute the cleaning process + */ + public void skippedAsCleanCache() { + nbskippedAsCleanCache++; + } + + public int getSkippedAsCleanCache() { + return nbskippedAsCleanCache; + } + + public void checkedButAlreadyClean() { + nbCheckedButAlreadyClean++; + } + + public int getCheckedButAlreadyClean() { + return nbCheckedButAlreadyClean; + } + + public void cleaned() { + nbCleaned++; + } + + public int getCleaned() { + return nbCleaned; + } + + public int getTotal() { + return nbskippedAsCleanCache + nbCheckedButAlreadyClean + nbCleaned; + } +} 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 6ea67240ee..608dca0ef8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,10 @@ import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import com.diffplug.spotless.DirtyState; import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.PaddedCell; import com.diffplug.spotless.maven.incremental.UpToDateChecker; /** @@ -31,10 +32,27 @@ @Mojo(name = AbstractSpotlessMojo.GOAL_APPLY, threadSafe = true) public class SpotlessApplyMojo extends AbstractSpotlessMojo { + @Parameter(property = "spotlessIdeHook") + private String spotlessIdeHook; + + @Parameter(property = "spotlessIdeHookUseStdIn") + private boolean spotlessIdeHookUseStdIn; + + @Parameter(property = "spotlessIdeHookUseStdOut") + private boolean spotlessIdeHookUseStdOut; + @Override - protected void process(Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + if (isIdeHook()) { + IdeHook.performHook(files, formatter, spotlessIdeHook, spotlessIdeHookUseStdIn, spotlessIdeHookUseStdOut); + return; + } + + ImpactedFilesTracker counter = new ImpactedFilesTracker(); + for (File file : files) { if (upToDateChecker.isUpToDate(file.toPath())) { + counter.skippedAsCleanCache(); if (getLog().isDebugEnabled()) { getLog().debug("Spotless will not format an up-to-date file: " + file); } @@ -42,15 +60,32 @@ protected void process(Iterable files, Formatter formatter, UpToDateChecke } try { - PaddedCell.DirtyState dirtyState = PaddedCell.calculateDirtyState(formatter, file); + DirtyState dirtyState = DirtyState.of(formatter, file); if (!dirtyState.isClean() && !dirtyState.didNotConverge()) { + getLog().info(String.format("clean file: %s", file)); dirtyState.writeCanonicalTo(file); + buildContext.refresh(file); + counter.cleaned(); + } else { + counter.checkedButAlreadyClean(); } - } catch (IOException e) { + } catch (IOException | RuntimeException e) { throw new MojoExecutionException("Unable to format file " + file, e); } upToDateChecker.setUpToDate(file.toPath()); } + + // We print the number of considered files which is useful when ratchetFrom is setup + if (counter.getTotal() > 0) { + getLog().info(String.format("Spotless.%s is keeping %s files clean - %s were changed to be clean, %s were already clean, %s were skipped because caching determined they were already clean", + name, counter.getTotal(), counter.getCleaned(), counter.getCheckedButAlreadyClean(), counter.getSkippedAsCleanCache())); + } else { + getLog().debug(String.format("Spotless.%s has no target files. Examine your ``: https://github.com/diffplug/spotless/tree/main/plugin-maven#quickstart", name)); + } + } + + private boolean isIdeHook() { + return !(spotlessIdeHook == null || spotlessIdeHook.isEmpty()); } } 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 ad3230f583..116a24dcf4 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-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.sonatype.plexus.build.incremental.BuildContext; +import com.diffplug.spotless.DirtyState; import com.diffplug.spotless.Formatter; -import com.diffplug.spotless.PaddedCell; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; import com.diffplug.spotless.maven.incremental.UpToDateChecker; @@ -36,33 +39,74 @@ @Mojo(name = AbstractSpotlessMojo.GOAL_CHECK, defaultPhase = LifecyclePhase.VERIFY, threadSafe = true) public class SpotlessCheckMojo extends AbstractSpotlessMojo { + private static final String INCREMENTAL_MESSAGE_PREFIX = "Spotless Violation: "; + + public enum MessageSeverity { + WARNING(BuildContext.SEVERITY_WARNING), ERROR(BuildContext.SEVERITY_ERROR); + + private final int severity; + + MessageSeverity(int severity) { + this.severity = severity; + } + + public int getSeverity() { + return severity; + } + } + + /** + * The severity used to emit messages during incremental builds. + * Either {@code WARNING} or {@code ERROR}. + * @see AbstractSpotlessMojo#m2eEnableForIncrementalBuild + */ + @Parameter(defaultValue = "WARNING") + private MessageSeverity m2eIncrementalBuildMessageSeverity; + @Override - protected void process(Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + ImpactedFilesTracker counter = new ImpactedFilesTracker(); + List problemFiles = new ArrayList<>(); for (File file : files) { if (upToDateChecker.isUpToDate(file.toPath())) { + counter.skippedAsCleanCache(); if (getLog().isDebugEnabled()) { getLog().debug("Spotless will not check an up-to-date file: " + file); } continue; } - + buildContext.removeMessages(file); try { - PaddedCell.DirtyState dirtyState = PaddedCell.calculateDirtyState(formatter, file); + DirtyState dirtyState = DirtyState.of(formatter, file); if (!dirtyState.isClean() && !dirtyState.didNotConverge()) { problemFiles.add(file); + if (buildContext.isIncremental()) { + Map.Entry diffEntry = DiffMessageFormatter.diff(baseDir.toPath(), formatter, file); + buildContext.addMessage(file, diffEntry.getKey() + 1, 0, INCREMENTAL_MESSAGE_PREFIX + diffEntry.getValue(), m2eIncrementalBuildMessageSeverity.getSeverity(), null); + } + counter.cleaned(); } else { + counter.checkedButAlreadyClean(); upToDateChecker.setUpToDate(file.toPath()); } - } catch (IOException e) { - throw new MojoExecutionException("Unable to format file " + file, e); + } catch (IOException | RuntimeException e) { + throw new MojoExecutionException("Unable to check file " + file, e); } } + // We print the number of considered files which is useful when ratchetFrom is setup + if (counter.getTotal() > 0) { + getLog().info(String.format("Spotless.%s is keeping %s files clean - %s needs changes to be clean, %s were already clean, %s were skipped because caching determined they were already clean", + name, counter.getTotal(), counter.getCleaned(), counter.getCheckedButAlreadyClean(), counter.getSkippedAsCleanCache())); + } else { + getLog().debug(String.format("Spotless.%s has no target files. Examine your ``: https://github.com/diffplug/spotless/tree/main/plugin-maven#quickstart", name)); + } + if (!problemFiles.isEmpty()) { throw new MojoExecutionException(DiffMessageFormatter.builder() .runToFix("Run 'mvn spotless:apply' to fix these violations.") - .formatter(formatter) + .formatter(baseDir.toPath(), formatter) .problemFiles(problemFiles) .getMessage()); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/antlr4/Antlr4.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/antlr4/Antlr4.java index bc24ebcf16..a43416fb4e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/antlr4/Antlr4.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/antlr4/Antlr4.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 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.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.common.collect.ImmutableSet; import com.diffplug.spotless.antlr4.Antlr4Defaults; import com.diffplug.spotless.maven.FormatterFactory; @@ -30,7 +32,7 @@ */ public class Antlr4 extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return ImmutableSet.of(Antlr4Defaults.includes()); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Clang.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Clang.java new file mode 100644 index 0000000000..f4db16049a --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Clang.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024-2025 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.cpp; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.cpp.ClangFormatStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class Clang implements FormatterStepFactory { + @Parameter + private String version; + + @Parameter + private String pathToExe; + + @Parameter + private String style; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + ClangFormatStep clang = ClangFormatStep.withVersion(version == null ? ClangFormatStep.defaultVersion() : version); + + if (pathToExe != null) { + clang = clang.withPathToExe(pathToExe); + } + + if (style != null) { + clang = clang.withStyle(style); + } + + return clang.create(); + } + +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Cpp.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Cpp.java index f1d07d8552..8739a1c717 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Cpp.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/Cpp.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.cpp.CppDefaults; import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -30,7 +32,7 @@ */ public class Cpp extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } @@ -38,6 +40,10 @@ public void addEclipseCdt(EclipseCdt eclipse) { addStepFactory(eclipse); } + public void addClangFormat(Clang clang) { + addStepFactory(clang); + } + @Override public String licenseHeaderDelimiter() { return CppDefaults.DELIMITER_EXPR; diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java index 78484abfba..92f4229ed3 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/cpp/EclipseCdt.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package com.diffplug.spotless.maven.cpp; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.apache.maven.plugins.annotations.Parameter; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Mirror; import com.diffplug.spotless.extra.cpp.EclipseCdtFormatterStep; import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; @@ -34,14 +37,18 @@ public class EclipseCdt implements FormatterStepFactory { @Parameter private String version; + @Parameter + private List p2Mirrors = new ArrayList<>(); + @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EclipseBasedStepBuilder eclipseConfig = EclipseCdtFormatterStep.createBuilder(stepConfig.getProvisioner()); + EquoBasedStepBuilder eclipseConfig = EclipseCdtFormatterStep.createBuilder(stepConfig.getProvisioner()); eclipseConfig.setVersion(version == null ? EclipseCdtFormatterStep.defaultVersion() : version); if (null != file) { File settingsFile = stepConfig.getFileLocator().locateFile(file); eclipseConfig.setPreferences(Arrays.asList(settingsFile)); } + eclipseConfig.setP2Mirrors(p2Mirrors); return eclipseConfig.build(); } } diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/package-info.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/BiomeCss.java similarity index 59% rename from _ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/package-info.java rename to plugin-maven/src/main/java/com/diffplug/spotless/maven/css/BiomeCss.java index 931b7e085e..ca96f47cfe 100644 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/sse/package-info.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/BiomeCss.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Eclipse WTP Structured Source Editing (SEE) formatter helper */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.wtp.sse; +package com.diffplug.spotless.maven.css; -import javax.annotation.ParametersAreNonnullByDefault; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.maven.generic.AbstractBiome; + +/** + * Biome formatter step for CSS. + */ +public class BiomeCss extends AbstractBiome { + public BiomeCss() { + super(BiomeFlavor.BIOME); + } + + @Override + protected String getLanguage() { + return "css"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/Css.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/Css.java new file mode 100644 index 0000000000..4d465d8939 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/css/Css.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023-2024 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.css; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class Css extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addBiome(BiomeCss biome) { + addStepFactory(biome); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/AbstractBiome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/AbstractBiome.java new file mode 100644 index 0000000000..f1473adf45 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/AbstractBiome.java @@ -0,0 +1,196 @@ +/* + * Copyright 2016-2024 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.generic; + +import java.nio.file.Paths; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.biome.BiomeStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +/** + * Factory for creating the Biome formatter step that can format format code in + * various types of language with Biome. Currently Biome supports JavaScript, + * TypeScript, JSX, TSX, and JSON. See also https://github.com/biomejs/biome. It + * delegates to the Biome CLI executable. + */ +public abstract class AbstractBiome implements FormatterStepFactory { + /** Biome flavor to use. */ + private BiomeFlavor flavor; + + protected AbstractBiome(BiomeFlavor flavor) { + this.flavor = flavor; + } + + /** + * Optional path to the directory with configuration file for Biome. The file + * must be named {@code biome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + */ + @Parameter + private String configPath; + + /** + * Optional directory where the downloaded Biome executable is placed. If this + * is a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/biome. + *

+ * You can use an expression like ${user.home}/biome if you want to + * use the home directory, or ${project.build.directory if you want + * to use the target directory of the current project. + */ + @Parameter + private String downloadDir; + + /** + * Optional path to the Biome executable. Either a version or a + * pathToExe should be specified. When not given, an attempt is + * made to download the executable for the given version from the network. When + * given, the executable is used and the version parameter is + * ignored. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * biome) is given, this is interpreted as the name of a command + * with executable that is in your path environment variable. Use + * ./executable-name if you want to use an executable in the + * project's base directory. + */ + @Parameter + private String pathToExe; + + /** + * Biome version to download, applies only when no pathToExe is + * specified explicitly. Either a version or a + * pathToExe should be specified. When not given, a default known + * version is used. For stable builds, it is recommended that you always set the + * version explicitly. This parameter is ignored when you specify a + * pathToExe explicitly. + */ + @Parameter + private String version; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + var builder = newBuilder(config); + if (configPath != null) { + var resolvedConfigFile = resolveConfigFile(config); + builder.withConfigPath(resolvedConfigFile); + } + if (getLanguage() != null) { + builder.withLanguage(getLanguage()); + } + return builder.create(); + } + + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Biome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ * + * @return The language of the input files. + */ + protected abstract String getLanguage(); + + /** + * A new builder for configuring a Biome step that either downloads the Biome + * executable with the given version from the network, or uses the executable + * from the given path. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return A builder for a Biome step. + */ + private BiomeStep newBuilder(FormatterStepConfig config) { + if (pathToExe != null) { + var resolvedExePath = resolveExePath(config); + return BiomeStep.withExePath(flavor, resolvedExePath); + } else { + var downloadDir = resolveDownloadDir(config); + return BiomeStep.withExeDownload(flavor, version, downloadDir); + } + } + + /** + * Resolves the path to the configuration file for Biome. Relative paths are + * resolved against the project's base directory. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return The resolved path to the configuration file. + */ + private String resolveConfigFile(FormatterStepConfig config) { + return config.getFileLocator().getBaseDir().toPath().resolve(configPath).toAbsolutePath().toString(); + } + + /** + * Resolves the path to the Biome executable. When the path is only a file name, + * do not perform any resolution and interpret it as a command that must be on + * the user's path. Otherwise resolve the executable path against the project's + * base directory. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return The resolved path to the Biome executable. + */ + private String resolveExePath(FormatterStepConfig config) { + var path = Paths.get(pathToExe); + if (path.getNameCount() == 1) { + return path.toString(); + } else { + return config.getFileLocator().getBaseDir().toPath().resolve(path).toAbsolutePath().toString(); + } + } + + /** + * Resolves the directory to use for storing downloaded Biome executable. When a + * {@link #downloadDir} is given, use that directory, resolved against the + * current project's directory. Otherwise, use the biome sub folder + * in the shared data directory. + * + * @param config Configuration for this step. + * @return The download directory for the Biome executable. + */ + private String resolveDownloadDir(FormatterStepConfig config) { + final var fileLocator = config.getFileLocator(); + if (downloadDir != null && !downloadDir.isBlank()) { + return fileLocator.getBaseDir().toPath().resolve(downloadDir).toAbsolutePath().toString(); + } else { + return fileLocator.getDataDir().toPath().resolve(flavor.shortName()).toString(); + } + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Biome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Biome.java new file mode 100644 index 0000000000..99cede1065 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Biome.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2024 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.generic; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.biome.BiomeFlavor; + +/** + * Generic Biome formatter step that detects the language of the input file from + * the file name. It should be specified as a formatter step for a generic + * {@code }. + */ +public class Biome extends AbstractBiome { + public Biome() { + super(BiomeFlavor.BIOME); + } + + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently, the following languages are supported by Biome: + *
    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • css (CSS, requires biome >= 1.9.0)
  • + *
  • json (JSON)
  • + *
  • jsonc (JSON + comments)
  • + *
+ * + * @return The language of the input files. + */ + @Parameter + private String language; + + @Override + protected String getLanguage() { + return language; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java index 465381fb52..ccdef479cf 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.maven.FormatterFactory; /** @@ -29,7 +31,7 @@ public class Format extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } @@ -38,4 +40,12 @@ public String licenseHeaderDelimiter() { // do not specify a default delimiter return null; } + + /** + * Adds a step to this format that format code with the Biome formatter. + * @param biome Biome configuration to use. + */ + public void addBiome(Biome biome) { + addStepFactory(biome); + } } 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 ee2ae4fda0..ff89f55ead 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,9 @@ public class LicenseHeader implements FormatterStepFactory { @Parameter private String delimiter; + @Parameter + private String skipLinesMatching; + @Override public final FormatterStep newFormatterStep(FormatterStepConfig config) { String delimiterString = delimiter != null ? delimiter : config.getLicenseHeaderDelimiter(); @@ -53,6 +56,7 @@ public final FormatterStep newFormatterStep(FormatterStepConfig config) { } return LicenseHeaderStep.headerDelimiter(() -> readFileOrContent(config), delimiterString) .withYearMode(yearMode) + .withSkipLinesMatching(skipLinesMatching) .build() .filterByFile(LicenseHeaderStep.unsupportedJvmFilesFilter()); } else { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java index 85684e85cd..c6b3e46e3c 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,12 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.maven.FormatterStepConfig; -import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.maven.npm.AbstractNpmFormatterStepFactory; import com.diffplug.spotless.npm.NpmPathResolver; import com.diffplug.spotless.npm.PrettierConfig; import com.diffplug.spotless.npm.PrettierFormatterStep; -public class Prettier implements FormatterStepFactory { +public class Prettier extends AbstractNpmFormatterStepFactory { public static final String ERROR_MESSAGE_ONLY_ONE_CONFIG = "must specify exactly one prettierVersion, devDependencies or devDependencyProperties"; @@ -47,12 +47,6 @@ public class Prettier implements FormatterStepFactory { @Parameter private String configFile; - @Parameter - private String npmExecutable; - - @Parameter - private String npmrc; - @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { @@ -67,13 +61,9 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { if (prettierVersion != null && !prettierVersion.isEmpty()) { this.devDependencies = PrettierFormatterStep.defaultDevDependenciesWithPrettier(prettierVersion); } else if (devDependencyProperties != null) { - this.devDependencies = dependencyPropertiesAsMap(); + this.devDependencies = propertiesAsMap(this.devDependencyProperties); } - File npm = npmExecutable != null ? stepConfig.getFileLocator().locateFile(npmExecutable) : null; - - File npmrcFile = npmrc != null ? stepConfig.getFileLocator().locateFile(npmrc) : null; - // process config file or inline config File configFileHandler; if (this.configFile != null) { @@ -95,6 +85,11 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { if (Boolean.TRUE.toString().equalsIgnoreCase(entry.getValue()) || Boolean.FALSE.toString().equalsIgnoreCase(entry.getValue())) { return new AbstractMap.SimpleEntry<>(entry.getKey(), Boolean.parseBoolean(entry.getValue())); } + // Prettier v3 - plugins config will be a comma delimited list of plugins + if (entry.getKey().equals("plugins")) { + List values = entry.getValue().isEmpty() ? List.of() : Arrays.asList(entry.getValue().split(",")); + return new AbstractMap.SimpleEntry<>(entry.getKey(), values); + } return entry; }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); @@ -103,24 +98,12 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { } // create the format step + File baseDir = baseDir(stepConfig); + File buildDir = buildDir(stepConfig); + File cacheDir = cacheDir(stepConfig); PrettierConfig prettierConfig = new PrettierConfig(configFileHandler, configInline); - File buildDir = stepConfig.getFileLocator().getBuildDir(); - NpmPathResolver npmPathResolver = new NpmPathResolver(npm, npmrcFile, stepConfig.getFileLocator().getBaseDir()); - return PrettierFormatterStep.create(devDependencies, stepConfig.getProvisioner(), buildDir, npmPathResolver, prettierConfig); - } - - private boolean moreThanOneNonNull(Object... objects) { - return Arrays.stream(objects) - .filter(Objects::nonNull) - .filter(o -> !(o instanceof String) || !((String) o).isEmpty()) // if it is a string, it should not be empty - .count() > 1; - } - - private Map dependencyPropertiesAsMap() { - return this.devDependencyProperties.stringPropertyNames() - .stream() - .map(name -> new AbstractMap.SimpleEntry<>(name, this.devDependencyProperties.getProperty(name))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + NpmPathResolver npmPathResolver = npmPathResolver(stepConfig); + return PrettierFormatterStep.create(devDependencies, stepConfig.getProvisioner(), baseDir, buildDir, cacheDir, npmPathResolver, prettierConfig); } private static IllegalArgumentException onlyOneConfig() { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Replace.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Replace.java index 947ceee8e2..53967fd7d7 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Replace.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Replace.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,12 @@ public class Replace implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { - if (name == null || search == null || replacement == null) { - throw new IllegalArgumentException("Must specify 'name', 'search' and 'replacement'."); + if (name == null || search == null) { + throw new IllegalArgumentException("Must specify 'name' and 'search'."); } - - return ReplaceStep.create(name, search, replacement); + // Use empty string if replacement is not provided. In pom.xml there is no way to specify + // an empty string as a property value as maven will always trim the value and if it is + // empty, maven will consider the property as not provided. + return ReplaceStep.create(name, search, replacement != null ? replacement : ""); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ReplaceRegex.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ReplaceRegex.java index 243f14220e..9983aa8475 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ReplaceRegex.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ReplaceRegex.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,12 @@ public class ReplaceRegex implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { - if (name == null || searchRegex == null || replacement == null) { - throw new IllegalArgumentException("Must specify 'name', 'searchRegex' and 'replacement'."); + if (name == null || searchRegex == null) { + throw new IllegalArgumentException("Must specify 'name' and 'searchRegex'."); } - - return ReplaceRegexStep.create(name, searchRegex, replacement); + // Use empty string if replacement is not provided. In pom.xml there is no way to specify + // an empty string as a property value as maven will always trim the value and if it is + // empty, maven will consider the property as not provided. + return ReplaceRegexStep.create(name, searchRegex, replacement != null ? replacement : ""); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java index fe1676aec9..984a82601f 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,23 @@ import org.apache.maven.plugins.annotations.Parameter; -import com.diffplug.spotless.generic.PipeStepPair; +import com.diffplug.spotless.generic.FenceStep; public class ToggleOffOn { @Parameter - public String off = PipeStepPair.defaultToggleOff(); + public String off = FenceStep.defaultToggleOff(); @Parameter - public String on = PipeStepPair.defaultToggleOn(); + public String on = FenceStep.defaultToggleOn(); @Parameter public String regex; - public PipeStepPair createPair() { + public FenceStep createFence() { if (regex != null) { - return PipeStepPair.named(PipeStepPair.defaultToggleName()).regex(regex).buildPair(); + return FenceStep.named(FenceStep.defaultToggleName()).regex(regex); } else { - return PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair(); + return FenceStep.named(FenceStep.defaultToggleName()).openClose(off, on); } } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/Gherkin.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/Gherkin.java new file mode 100644 index 0000000000..60181d048a --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/Gherkin.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.gherkin; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class Gherkin extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addGherkinUtils(GherkinUtils gherkinUtils) { + addStepFactory(gherkinUtils); + } + +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/GherkinUtils.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/GherkinUtils.java new file mode 100644 index 0000000000..2eeabb9ab5 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/gherkin/GherkinUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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.gherkin; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.gherkin.GherkinUtilsConfig; +import com.diffplug.spotless.gherkin.GherkinUtilsStep; +import com.diffplug.spotless.maven.FormatterFactory; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class GherkinUtils implements FormatterStepFactory { + + @Parameter + private String version = GherkinUtilsStep.defaultVersion(); + + @Parameter + private int indentWithSpaces = GherkinUtilsConfig.defaultIndentSpaces(); + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + return GherkinUtilsStep.create(new GherkinUtilsConfig(indentWithSpaces), version, stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Go.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Go.java new file mode 100644 index 0000000000..bb26bd0355 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Go.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.go; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +public class Go extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addGofmt(Gofmt gofmt) { + addStepFactory(gofmt); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Gofmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Gofmt.java new file mode 100644 index 0000000000..aaa30efb4f --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/go/Gofmt.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.go; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.go.GofmtFormatStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class Gofmt implements FormatterStepFactory { + + @Parameter + private String version; + + @Parameter + private String goExecutablePath; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + GofmtFormatStep step = GofmtFormatStep.withVersion(version == null ? GofmtFormatStep.defaultVersion() : version); + if (goExecutablePath != null) { + step = step.withGoExecutable(goExecutablePath); + } + return step.create(); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java index 81da109e04..67ab40fbf2 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/GrEclipse.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package com.diffplug.spotless.maven.groovy; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.apache.maven.plugins.annotations.Parameter; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.P2Mirror; import com.diffplug.spotless.extra.groovy.GrEclipseFormatterStep; import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; @@ -34,14 +37,18 @@ public class GrEclipse implements FormatterStepFactory { @Parameter private String version; + @Parameter + private List p2Mirrors = new ArrayList<>(); + @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EclipseBasedStepBuilder grEclipseConfig = GrEclipseFormatterStep.createBuilder(stepConfig.getProvisioner()); + EquoBasedStepBuilder grEclipseConfig = GrEclipseFormatterStep.createBuilder(stepConfig.getProvisioner()); grEclipseConfig.setVersion(version == null ? GrEclipseFormatterStep.defaultVersion() : version); if (null != file) { File settingsFile = stepConfig.getFileLocator().locateFile(file); grEclipseConfig.setPreferences(Arrays.asList(settingsFile)); } + grEclipseConfig.setP2Mirrors(p2Mirrors); return grEclipseConfig.build(); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java index e9b3c6c412..db4b35dfd6 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.common.collect.ImmutableSet; +import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -30,10 +33,10 @@ public class Groovy extends FormatterFactory { private static final Set DEFAULT_INCLUDES = ImmutableSet.of("src/main/groovy/**/*.groovy", "src/test/groovy/**/*.groovy"); - private static final String LICENSE_HEADER_DELIMITER = "package "; + private static final String LICENSE_HEADER_DELIMITER = LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return DEFAULT_INCLUDES; } @@ -49,4 +52,8 @@ public void addGreclipse(GrEclipse greclipse) { public void addImportOrder(ImportOrder importOrder) { addStepFactory(importOrder); } + + public void addRemoveSemicolons(RemoveSemicolons removeSemicolons) { + addStepFactory(removeSemicolons); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/RemoveSemicolons.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/RemoveSemicolons.java new file mode 100644 index 0000000000..a96a078692 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/RemoveSemicolons.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2023 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.groovy; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.groovy.RemoveSemicolonsStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class RemoveSemicolons implements FormatterStepFactory { + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + return RemoveSemicolonsStep.create(); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/FileIndex.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/FileIndex.java index 728f8a57bc..c719419923 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/FileIndex.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/FileIndex.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.io.PrintWriter; import java.io.UncheckedIOException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -76,7 +77,7 @@ static FileIndex read(FileIndexConfig config, Log log) { PluginFingerprint computedFingerprint = config.getPluginFingerprint(); PluginFingerprint storedFingerprint = PluginFingerprint.from(firstLine); if (!computedFingerprint.equals(storedFingerprint)) { - log.info("Fingerprint mismatch in the index file. Fallback to an empty index"); + log.info("Index file corresponds to a different configuration of the plugin. Either the plugin version or its configuration has changed. Fallback to an empty index"); return emptyIndexFallback(config); } else { Content content = readIndexContent(reader, config.getProjectDir(), log); @@ -144,7 +145,14 @@ private void ensureParentDirExists() { throw new IllegalStateException("Index file does not have a parent dir: " + indexFile); } try { - Files.createDirectories(parentDir); + if (Files.exists(parentDir, LinkOption.NOFOLLOW_LINKS)) { + Path realPath = parentDir.toRealPath(); + if (!Files.exists(realPath)) { + Files.createDirectories(realPath); + } + } else { + Files.createDirectories(parentDir); + } } catch (IOException e) { throw new UncheckedIOException("Unable to create parent directory for the index file: " + indexFile, e); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/PluginFingerprint.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/PluginFingerprint.java index 3805cf4600..34a01e235a 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/PluginFingerprint.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/PluginFingerprint.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,22 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.util.ArrayList; import java.util.Base64; -import java.util.List; import java.util.Objects; -import org.apache.maven.model.Dependency; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginManagement; import org.apache.maven.project.MavenProject; import com.diffplug.spotless.Formatter; +/** + * Represents a particular Spotless Maven plugin setup using a Base64-encoded serialized form of: + *
    + *
  1. Plugin version as configured in the POM
  2. + *
  3. Formatter instances created according to the POM configuration
  4. + *
+ */ class PluginFingerprint { private static final String SPOTLESS_PLUGIN_KEY = "com.diffplug.spotless:spotless-maven-plugin"; @@ -39,10 +44,7 @@ private PluginFingerprint(String value) { } static PluginFingerprint from(MavenProject project, Iterable formatters) { - Plugin spotlessPlugin = project.getPlugin(SPOTLESS_PLUGIN_KEY); - if (spotlessPlugin == null) { - throw new IllegalArgumentException("Spotless plugin absent from the project: " + project); - } + Plugin spotlessPlugin = findSpotlessPlugin(project); byte[] digest = digest(spotlessPlugin, formatters); String value = Base64.getEncoder().encodeToString(digest); return new PluginFingerprint(value); @@ -82,13 +84,27 @@ public String toString() { return "PluginFingerprint[" + value + "]"; } + private static Plugin findSpotlessPlugin(MavenProject project) { + // Try to find the plugin instance from XML element + Plugin plugin = project.getPlugin(SPOTLESS_PLUGIN_KEY); + if (plugin == null) { + // Try to find the plugin instance from XML element. Useful when + // the current module is a parent of a multimodule project + PluginManagement pluginManagement = project.getPluginManagement(); + if (pluginManagement != null) { + plugin = pluginManagement.getPluginsAsMap().get(SPOTLESS_PLUGIN_KEY); + } + } + + if (plugin == null) { + throw new IllegalArgumentException("Spotless plugin absent from the project: " + project); + } + return plugin; + } + private static byte[] digest(Plugin plugin, Iterable formatters) { - // dependencies can be an unserializable org.apache.maven.model.merge.ModelMerger$MergingList - // replace it with a serializable ArrayList - List dependencies = plugin.getDependencies(); - plugin.setDependencies(new ArrayList<>(dependencies)); try (ObjectDigestOutputStream out = ObjectDigestOutputStream.create()) { - out.writeObject(plugin); + out.writeObject(plugin.getVersion()); for (Formatter formatter : formatters) { out.writeObject(formatter); } @@ -96,9 +112,6 @@ private static byte[] digest(Plugin plugin, Iterable formatters) { return out.digest(); } catch (IOException e) { throw new UncheckedIOException("Unable to serialize plugin " + plugin, e); - } finally { - // reset the original list - plugin.setDependencies(dependencies); } } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecker.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecker.java index 2fd2f3a1ea..40dc84c8c6 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecker.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecker.java @@ -19,6 +19,7 @@ import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; +import org.sonatype.plexus.build.incremental.BuildContext; import com.diffplug.spotless.Formatter; @@ -37,4 +38,28 @@ static UpToDateChecker noop(MavenProject project, Path indexFile, Log log) { static UpToDateChecker forProject(MavenProject project, Path indexFile, Iterable formatters, Log log) { return IndexBasedChecker.create(project, indexFile, formatters, log); } + + static UpToDateChecker wrapWithBuildContext(UpToDateChecker delegate, BuildContext buildContext) { + return new UpToDateChecker() { + + @Override + public void setUpToDate(Path file) { + delegate.setUpToDate(file); + } + + @Override + public boolean isUpToDate(Path file) { + if (buildContext.hasDelta(file.toFile())) { + return delegate.isUpToDate(file); + } + return true; + } + + @Override + public void close() { + delegate.close(); + } + }; + } + } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecking.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecking.java index d5eb6aa941..f044b847c9 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecking.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/incremental/UpToDateChecking.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,4 +38,10 @@ public boolean isEnabled() { public Path getIndexFile() { return indexFile == null ? null : new File(indexFile).toPath(); } + + public static UpToDateChecking enabled() { + UpToDateChecking upToDateChecking = new UpToDateChecking(); + upToDateChecking.enabled = true; + return upToDateChecking; + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/CleanthatJava.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/CleanthatJava.java new file mode 100644 index 0000000000..4133409919 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/CleanthatJava.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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 java.util.List; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.CleanthatJavaStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class CleanthatJava implements FormatterStepFactory { + @Parameter + private String groupArtifact; + + @Parameter + private String version; + + // https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#source + @Parameter(property = "maven.compiler.source") + private String sourceJdk = CleanthatJavaStep.defaultSourceJdk(); + + @Parameter + private List mutators = CleanthatJavaStep.defaultMutators(); + + @Parameter + private List excludedMutators = CleanthatJavaStep.defaultExcludedMutators(); + + @Parameter + private boolean includeDraft = CleanthatJavaStep.defaultIncludeDraft(); + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + String groupArtifact = this.groupArtifact != null ? this.groupArtifact : CleanthatJavaStep.defaultGroupArtifact(); + String version = this.version != null ? this.version : CleanthatJavaStep.defaultVersion(); + + return CleanthatJavaStep.create(groupArtifact, version, sourceJdk, mutators, excludedMutators, includeDraft, config.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java index 1e6508121e..5e795ac93f 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package com.diffplug.spotless.maven.java; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.apache.maven.plugins.annotations.Parameter; +import org.eclipse.aether.RepositorySystemSession; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.extra.EclipseBasedStepBuilder; +import com.diffplug.spotless.extra.P2Mirror; import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; @@ -34,14 +37,60 @@ public class Eclipse implements FormatterStepFactory { @Parameter private String version; + @Parameter + private List p2Mirrors = new ArrayList<>(); + + @Parameter + private Boolean sortMembersDoNotSortFields = true; + + @Parameter + private Boolean sortMembersEnabled = false; + + @Parameter + private String sortMembersOrder; + + @Parameter + private String sortMembersVisibilityOrder; + + @Parameter + private Boolean sortMembersVisibilityOrderEnabled = false; + + private File cacheDirectory; + @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { - EclipseBasedStepBuilder eclipseConfig = EclipseJdtFormatterStep.createBuilder(stepConfig.getProvisioner()); + EclipseJdtFormatterStep.Builder eclipseConfig = EclipseJdtFormatterStep.createBuilder(stepConfig.getProvisioner()); eclipseConfig.setVersion(version == null ? EclipseJdtFormatterStep.defaultVersion() : version); if (null != file) { File settingsFile = stepConfig.getFileLocator().locateFile(file); eclipseConfig.setPreferences(Arrays.asList(settingsFile)); } + eclipseConfig.setP2Mirrors(p2Mirrors); + if (null != cacheDirectory) { + eclipseConfig.setCacheDirectory(cacheDirectory); + } + if (null != sortMembersEnabled) { + eclipseConfig.sortMembersEnabled(sortMembersEnabled); + } + if (null != sortMembersOrder) { + eclipseConfig.sortMembersOrder(sortMembersOrder); + } + if (null != sortMembersDoNotSortFields) { + eclipseConfig.sortMembersDoNotSortFields(sortMembersDoNotSortFields); + } + if (null != sortMembersVisibilityOrder) { + eclipseConfig.sortMembersVisibilityOrder(sortMembersVisibilityOrder); + } + if (null != sortMembersVisibilityOrderEnabled) { + eclipseConfig.sortMembersVisibilityOrderEnabled(sortMembersVisibilityOrderEnabled); + } + if (null != cacheDirectory) { + eclipseConfig.setCacheDirectory(cacheDirectory); + } return eclipseConfig.build(); } + + public void init(RepositorySystemSession repositorySystemSession) { + this.cacheDirectory = repositorySystemSession.getLocalRepository().getBasedir(); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java new file mode 100644 index 0000000000..ac60af0a62 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 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 java.util.Collections; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.FormatAnnotationsStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class FormatAnnotations implements FormatterStepFactory { + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + // TODO: Permit customization in Maven build files. + return FormatAnnotationsStep.create(Collections.emptyList(), Collections.emptyList()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java index 3597e84e25..d0a2b24954 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,20 @@ public class GoogleJavaFormat implements FormatterStepFactory { @Parameter private Boolean reflowLongStrings; + @Parameter + private Boolean reorderImports; + + @Parameter + private Boolean formatJavadoc; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String groupArtifact = this.groupArtifact != null ? this.groupArtifact : GoogleJavaFormatStep.defaultGroupArtifact(); String version = this.version != null ? this.version : GoogleJavaFormatStep.defaultVersion(); String style = this.style != null ? this.style : GoogleJavaFormatStep.defaultStyle(); boolean reflowLongStrings = this.reflowLongStrings != null ? this.reflowLongStrings : GoogleJavaFormatStep.defaultReflowLongStrings(); - return GoogleJavaFormatStep.create(groupArtifact, version, style, config.getProvisioner(), reflowLongStrings); + boolean reorderImports = this.reorderImports != null ? this.reorderImports : GoogleJavaFormatStep.defaultReorderImports(); + boolean formatJavadoc = this.formatJavadoc != null ? this.formatJavadoc : GoogleJavaFormatStep.defaultFormatJavadoc(); + return GoogleJavaFormatStep.create(groupArtifact, version, style, config.getProvisioner(), reflowLongStrings, reorderImports, formatJavadoc); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ImportOrder.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ImportOrder.java index 8476b83733..88f47a2a64 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ImportOrder.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/ImportOrder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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.maven.java; import java.io.File; +import java.util.HashSet; +import java.util.Set; import org.apache.maven.plugins.annotations.Parameter; @@ -34,17 +36,43 @@ public class ImportOrder implements FormatterStepFactory { @Parameter private boolean wildcardsLast = false; + /** + * Whether imports should be sorted based on semantics (i.e. sorted by package, + * class and then static member). This considers treatAsPackage and + * treatAsClass, and assumes default upper and lower case + * conventions otherwise. When turned off, imports are sorted purely + * lexicographically. + */ + @Parameter + private boolean semanticSort = false; + + /** + * The prefixes that should be treated as packages for + * semanticSort. Useful for upper case package names. + */ + @Parameter + private Set treatAsPackage = new HashSet<>(); + + /** + * The prefixes that should be treated as classes for + * semanticSort. Useful for lower case class names. + */ + @Parameter + private Set treatAsClass = new HashSet<>(); + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { if (file != null ^ order != null) { if (file != null) { File importsFile = config.getFileLocator().locateFile(file); - return ImportOrderStep.forJava().createFrom(wildcardsLast, importsFile); + return ImportOrderStep.forJava().createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, + importsFile); } else { - return ImportOrderStep.forJava().createFrom(wildcardsLast, order.split(",", -1)); + return ImportOrderStep.forJava().createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass, + order.split(",", -1)); } } else if (file == null && order == null) { - return ImportOrderStep.forJava().createFrom(wildcardsLast); + return ImportOrderStep.forJava().createFrom(wildcardsLast, semanticSort, treatAsPackage, treatAsClass); } else { throw new IllegalArgumentException("Must specify exactly one of 'file' or 'order'."); } 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 819179ebba..b0692446b7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,18 @@ */ package com.diffplug.spotless.maven.java; +import static java.util.stream.Collectors.toSet; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Set; +import java.util.stream.Stream; + +import org.apache.maven.model.Build; +import org.apache.maven.project.MavenProject; -import com.diffplug.common.collect.ImmutableSet; +import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -29,12 +38,17 @@ */ public class Java extends FormatterFactory { - private static final Set DEFAULT_INCLUDES = ImmutableSet.of("src/main/java/**/*.java", "src/test/java/**/*.java"); - private static final String LICENSE_HEADER_DELIMITER = "package "; + private static final String LICENSE_HEADER_DELIMITER = LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; @Override - public Set defaultIncludes() { - return DEFAULT_INCLUDES; + public Set defaultIncludes(MavenProject project) { + Path projectDir = project.getBasedir().toPath(); + Build build = project.getBuild(); + return Stream.of(build.getSourceDirectory(), build.getTestSourceDirectory()) + .map(Paths::get) + .map(projectDir::relativize) + .map(Java::fileMask) + .collect(toSet()); } @Override @@ -61,4 +75,20 @@ public void addPalantirJavaFormat(PalantirJavaFormat palantirJavaFormat) { public void addRemoveUnusedImports(RemoveUnusedImports removeUnusedImports) { addStepFactory(removeUnusedImports); } + + public void addFormatAnnotations(FormatAnnotations formatAnnotations) { + addStepFactory(formatAnnotations); + } + + public void addCleanthat(CleanthatJava cleanthat) { + addStepFactory(cleanthat); + } + + private static String fileMask(Path path) { + String dir = path.toString(); + if (!dir.endsWith(File.separator)) { + dir += File.separator; + } + return dir + "**" + File.separator + "*.java"; + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java index df58764c1d..674238d2e8 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,9 +27,17 @@ public class PalantirJavaFormat implements FormatterStepFactory { @Parameter private String version; + @Parameter + private String style; + + @Parameter + private Boolean formatJavadoc; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String version = this.version != null ? this.version : PalantirJavaFormatStep.defaultVersion(); - return PalantirJavaFormatStep.create(version, config.getProvisioner()); + String style = this.style != null ? this.style : PalantirJavaFormatStep.defaultStyle(); + boolean formatJavadoc = this.formatJavadoc != null ? this.formatJavadoc : PalantirJavaFormatStep.defaultFormatJavadoc(); + return PalantirJavaFormatStep.create(version, style, formatJavadoc, config.getProvisioner()); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/RemoveUnusedImports.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/RemoveUnusedImports.java index 54fd30a167..85c6bbb8fe 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/RemoveUnusedImports.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/RemoveUnusedImports.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 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,8 @@ */ package com.diffplug.spotless.maven.java; +import org.apache.maven.plugins.annotations.Parameter; + import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; import com.diffplug.spotless.maven.FormatterStepConfig; @@ -22,8 +24,11 @@ public class RemoveUnusedImports implements FormatterStepFactory { + @Parameter + private String engine = RemoveUnusedImportsStep.defaultFormatter(); + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { - return RemoveUnusedImportsStep.create(config.getProvisioner()); + return RemoveUnusedImportsStep.create(engine, config.getProvisioner()); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/AbstractEslint.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/AbstractEslint.java new file mode 100644 index 0000000000..ad113de833 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/AbstractEslint.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2023 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.javascript; + +import java.io.File; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.npm.AbstractNpmFormatterStepFactory; +import com.diffplug.spotless.npm.EslintConfig; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.NpmPathResolver; + +public abstract class AbstractEslint extends AbstractNpmFormatterStepFactory { + + public static final String ERROR_MESSAGE_ONLY_ONE_CONFIG = "must specify exactly one eslintVersion, devDependencies or devDependencyProperties"; + + @Parameter + protected String configFile; + + @Parameter + protected String configJs; + + @Parameter + protected String eslintVersion; + + @Parameter + protected Map devDependencies; + + @Parameter + protected Properties devDependencyProperties; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + // check if config is only setup in one way + if (moreThanOneNonNull(this.eslintVersion, this.devDependencies, this.devDependencyProperties)) { + throw onlyOneConfig(); + } + + Map devDependencies = new TreeMap<>(); + if (this.devDependencies != null) { + devDependencies.putAll(this.devDependencies); + } else if (this.devDependencyProperties != null) { + devDependencies.putAll(propertiesAsMap(this.devDependencyProperties)); + } else { + Map defaultDependencies = createDefaultDependencies(); + devDependencies.putAll(defaultDependencies); + } + + File buildDir = buildDir(stepConfig); + File baseDir = baseDir(stepConfig); + File cacheDir = cacheDir(stepConfig); + NpmPathResolver npmPathResolver = npmPathResolver(stepConfig); + return EslintFormatterStep.create(devDependencies, stepConfig.getProvisioner(), baseDir, buildDir, cacheDir, npmPathResolver, eslintConfig(stepConfig)); + } + + private static IllegalArgumentException onlyOneConfig() { + return new IllegalArgumentException(ERROR_MESSAGE_ONLY_ONE_CONFIG); + } + + protected abstract EslintConfig eslintConfig(FormatterStepConfig stepConfig); + + protected abstract Map createDefaultDependencies(); +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/BiomeJs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/BiomeJs.java new file mode 100644 index 0000000000..b610a5b63b --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/BiomeJs.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2024 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.javascript; + +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.maven.generic.AbstractBiome; + +/** + * Biome formatter step for JavaScript. + */ +public class BiomeJs extends AbstractBiome { + public BiomeJs() { + super(BiomeFlavor.BIOME); + } + + @Override + protected String getLanguage() { + return "js?"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/EslintJs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/EslintJs.java new file mode 100644 index 0000000000..483d38ae1e --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/EslintJs.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2023 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.javascript; + +import java.util.Map; + +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.npm.EslintConfig; +import com.diffplug.spotless.npm.EslintFormatterStep; + +public class EslintJs extends AbstractEslint { + + protected EslintConfig eslintConfig(FormatterStepConfig stepConfig) { + return new EslintConfig(this.configFile != null ? stepConfig.getFileLocator().locateFile(this.configFile) : null, this.configJs); + } + + protected Map createDefaultDependencies() { + return this.eslintVersion == null ? EslintFormatterStep.defaultDevDependencies() : EslintFormatterStep.defaultDevDependenciesWithEslint(this.eslintVersion); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java new file mode 100644 index 0000000000..dc2949ae68 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2024 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.javascript; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + *

+ * It defines a formatter for typescript source files. + */ +public class Javascript extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addEslint(EslintJs eslint) { + addStepFactory(eslint); + } + + public void addBiome(BiomeJs biome) { + addStepFactory(biome); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/BiomeJson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/BiomeJson.java new file mode 100644 index 0000000000..f52b490d69 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/BiomeJson.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2024 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.json; + +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.maven.generic.AbstractBiome; + +/** + * Biome formatter step for JSON. + */ +public class BiomeJson extends AbstractBiome { + public BiomeJson() { + super(BiomeFlavor.BIOME); + } + + @Override + protected String getLanguage() { + return "json"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java new file mode 100644 index 0000000000..a76ddd396e --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2024 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.json; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.json.gson.GsonConfig; +import com.diffplug.spotless.json.gson.GsonStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class Gson implements FormatterStepFactory { + @Parameter + int indentSpaces = Json.DEFAULT_INDENTATION; + + @Parameter + boolean sortByKeys = false; + + @Parameter + boolean escapeHtml = false; + + @Parameter + String version = GsonStep.DEFAULT_VERSION; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + int indentSpaces = this.indentSpaces; + return GsonStep.create(new GsonConfig(sortByKeys, escapeHtml, indentSpaces, version), stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java new file mode 100644 index 0000000000..ecf925ddbe --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 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.json; + +import java.util.Collections; +import java.util.Map; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.json.JacksonJsonConfig; +import com.diffplug.spotless.json.JacksonJsonStep; +import com.diffplug.spotless.maven.FormatterFactory; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class JacksonJson implements FormatterStepFactory { + + @Parameter + private String version = JacksonJsonStep.defaultVersion(); + + @Parameter + private boolean spaceBeforeSeparator = new JacksonJsonConfig().isSpaceBeforeSeparator(); + + @Parameter + private Map features = Collections.emptyMap(); + + @Parameter + private Map jsonFeatures = Collections.emptyMap(); + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + JacksonJsonConfig jacksonConfig = new JacksonJsonConfig(); + + jacksonConfig.appendFeatureToToggle(features); + jacksonConfig.appendJsonFeatureToToggle(jsonFeatures); + jacksonConfig.setSpaceBeforeSeparator(spaceBeforeSeparator); + + return JacksonJsonStep + .create(jacksonConfig, version, stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java new file mode 100644 index 0000000000..80767f41f3 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023-2024 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.json; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class Json extends FormatterFactory { + public static final int DEFAULT_INDENTATION = 4; + + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addSimple(Simple simple) { + addStepFactory(simple); + } + + public void addGson(Gson gson) { + addStepFactory(gson); + } + + public void addJackson(JacksonJson jackson) { + addStepFactory(jackson); + } + + public void addBiome(BiomeJson biome) { + addStepFactory(biome); + } + + public void addJsonPatch(JsonPatch jsonPatch) { + addStepFactory(jsonPatch); + } +} 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 new file mode 100644 index 0000000000..a822ad09e2 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JsonPatch.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 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.json; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.json.JsonPatchStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +/** + * 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; + + @Parameter + String patch; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + return JsonPatchStep.create(zjsonPatchVersion, patch, stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Simple.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Simple.java new file mode 100644 index 0000000000..f1cc114da2 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Simple.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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.json; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.json.JsonSimpleStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class Simple implements FormatterStepFactory { + + @Parameter + int indentSpaces = Json.DEFAULT_INDENTATION; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + int indentSpaces = this.indentSpaces; + return JsonSimpleStep.create(indentSpaces, stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java index 97cbec07b2..e47345d45d 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package com.diffplug.spotless.maven.kotlin; -import java.util.Collections; - import org.apache.maven.plugins.annotations.Parameter; import com.diffplug.spotless.FileSignature; @@ -41,6 +39,6 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { config = ThrowingEx.get(() -> FileSignature.signAsList(stepConfig.getFileLocator().locateFile(configFile))); } String diktatVersion = version != null ? version : DiktatStep.defaultVersionDiktat(); - return DiktatStep.create(diktatVersion, stepConfig.getProvisioner(), Collections.emptyMap(), config); + return DiktatStep.create(diktatVersion, stepConfig.getProvisioner(), config); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java index cfb0aad39e..18eb13773d 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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,8 @@ import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.common.collect.ImmutableSet; import com.diffplug.spotless.maven.FormatterFactory; @@ -27,7 +29,7 @@ public class Kotlin extends FormatterFactory { private static final Set DEFAULT_INCLUDES = ImmutableSet.of("src/main/kotlin/**/*.kt", "src/test/kotlin/**/*.kt"); @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return DEFAULT_INCLUDES; } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java index c1258a15fe..4c5c4628d3 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 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 com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.kotlin.KtfmtStep; +import com.diffplug.spotless.kotlin.KtfmtStep.KtfmtFormattingOptions; import com.diffplug.spotless.kotlin.KtfmtStep.Style; import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; @@ -31,10 +32,26 @@ public class Ktfmt implements FormatterStepFactory { @Parameter private String style; + @Parameter + private Integer maxWidth; + + @Parameter + private Integer blockIndent; + + @Parameter + private Integer continuationIndent; + + @Parameter + private Boolean removeUnusedImports; + + @Parameter + private Boolean manageTrailingCommas; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String version = this.version != null ? this.version : KtfmtStep.defaultVersion(); - String style = this.style != null ? this.style : KtfmtStep.defaultStyle(); - return KtfmtStep.create(version, config.getProvisioner(), Style.valueOf(style)); + Style style = this.style != null ? Style.valueOf(this.style) : null; + KtfmtFormattingOptions options = new KtfmtFormattingOptions(maxWidth, blockIndent, continuationIndent, removeUnusedImports, manageTrailingCommas); + return KtfmtStep.create(version, config.getProvisioner(), style, options); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java index 796523d315..a54e0d9764 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,21 +15,54 @@ */ package com.diffplug.spotless.maven.kotlin; +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.maven.plugins.annotations.Parameter; +import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ThrowingEx; import com.diffplug.spotless.kotlin.KtLintStep; import com.diffplug.spotless.maven.FormatterStepConfig; import com.diffplug.spotless.maven.FormatterStepFactory; public class Ktlint implements FormatterStepFactory { + private static final File defaultEditorConfig = new File(".editorconfig"); @Parameter private String version; + @Parameter + private String editorConfigPath; + @Parameter + private Map editorConfigOverride; + @Parameter + private List customRuleSets; @Override - public FormatterStep newFormatterStep(FormatterStepConfig config) { + public FormatterStep newFormatterStep(final FormatterStepConfig stepConfig) { String ktlintVersion = version != null ? version : KtLintStep.defaultVersion(); - return KtLintStep.create(ktlintVersion, config.getProvisioner()); + FileSignature configPath = null; + if (editorConfigPath == null && defaultEditorConfig.exists()) { + editorConfigPath = defaultEditorConfig.getPath(); + } + if (editorConfigPath != null) { + configPath = ThrowingEx.get(() -> FileSignature.signAsList(stepConfig.getFileLocator().locateFile(editorConfigPath))); + } + if (editorConfigOverride == null) { + editorConfigOverride = new HashMap<>(); + } + if (customRuleSets == null) { + customRuleSets = Collections.emptyList(); + } + return KtLintStep.create( + ktlintVersion, + stepConfig.getProvisioner(), + configPath, + editorConfigOverride, + customRuleSets); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Markdown.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Markdown.java index 2ba9b1f58f..0941beb76a 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Markdown.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/markdown/Markdown.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -29,7 +31,7 @@ */ public class Markdown extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java new file mode 100644 index 0000000000..b0645c151e --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016-2023 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.npm; + +import java.io.File; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.npm.NpmPathResolver; + +public abstract class AbstractNpmFormatterStepFactory implements FormatterStepFactory { + + public static final String SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME = "spotless-npm-install-cache"; + + @Parameter + private String npmExecutable; + + @Parameter + private String nodeExecutable; + + @Parameter + private String npmrc; + + @Parameter + private String npmInstallCache; + + protected File npm(FormatterStepConfig stepConfig) { + File npm = npmExecutable != null ? stepConfig.getFileLocator().locateFile(npmExecutable) : null; + return npm; + } + + protected File node(FormatterStepConfig stepConfig) { + File node = nodeExecutable != null ? stepConfig.getFileLocator().locateFile(nodeExecutable) : null; + return node; + } + + protected File npmrc(FormatterStepConfig stepConfig) { + File npmrc = this.npmrc != null ? stepConfig.getFileLocator().locateFile(this.npmrc) : null; + return npmrc; + } + + protected File buildDir(FormatterStepConfig stepConfig) { + return stepConfig.getFileLocator().getBuildDir(); + } + + protected File cacheDir(FormatterStepConfig stepConfig) { + if (this.npmInstallCache == null) { + return null; + } + if ("true".equals(this.npmInstallCache.toLowerCase(Locale.ROOT))) { + return new File(buildDir(stepConfig), SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME); + } + return Paths.get(this.npmInstallCache).toFile(); + } + + protected File baseDir(FormatterStepConfig stepConfig) { + return stepConfig.getFileLocator().getBaseDir(); + } + + protected NpmPathResolver npmPathResolver(FormatterStepConfig stepConfig) { + return new NpmPathResolver(npm(stepConfig), node(stepConfig), npmrc(stepConfig), Collections.singletonList(baseDir(stepConfig))); + } + + protected boolean moreThanOneNonNull(Object... objects) { + return Arrays.stream(objects) + .filter(Objects::nonNull) + .filter(o -> !(o instanceof String) || !((String) o).isEmpty()) // if it is a string, it should not be empty + .count() > 1; + } + + protected Map propertiesAsMap(Properties devDependencyProperties) { + return devDependencyProperties.stringPropertyNames() + .stream() + .map(name -> new AbstractMap.SimpleEntry<>(name, devDependencyProperties.getProperty(name))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/Pom.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/Pom.java index f083a17c89..9a4d3eea06 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/Pom.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/Pom.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 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.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.common.collect.ImmutableSet; import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -29,7 +31,7 @@ */ public class Pom extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return ImmutableSet.of("pom.xml"); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/SortPom.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/SortPom.java index f4fe8cb96b..eba179cb1e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/SortPom.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/pom/SortPom.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,9 @@ public class SortPom implements FormatterStepFactory { private final SortPomCfg defaultValues = new SortPomCfg(); + @Parameter + String version = defaultValues.version; + @Parameter String encoding = defaultValues.encoding; @@ -41,6 +44,9 @@ public class SortPom implements FormatterStepFactory { @Parameter boolean keepBlankLines = defaultValues.keepBlankLines; + @Parameter + boolean endWithNewline = defaultValues.endWithNewline; + @Parameter int nrOfIndentSpace = defaultValues.nrOfIndentSpace; @@ -50,15 +56,24 @@ public class SortPom implements FormatterStepFactory { @Parameter boolean indentSchemaLocation = defaultValues.indentSchemaLocation; + @Parameter + String indentAttribute = defaultValues.indentAttribute; + @Parameter String predefinedSortOrder = defaultValues.predefinedSortOrder; + @Parameter + boolean quiet = defaultValues.quiet; + @Parameter String sortOrderFile = defaultValues.sortOrderFile; @Parameter String sortDependencies = defaultValues.sortDependencies; + @Parameter + String sortDependencyManagement = defaultValues.sortDependencyManagement; + @Parameter String sortDependencyExclusions = defaultValues.sortDependencyExclusions; @@ -77,17 +92,22 @@ public class SortPom implements FormatterStepFactory { @Override public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { SortPomCfg cfg = new SortPomCfg(); + cfg.version = version; cfg.encoding = encoding; cfg.lineSeparator = lineSeparator; cfg.expandEmptyElements = expandEmptyElements; cfg.spaceBeforeCloseEmptyElement = spaceBeforeCloseEmptyElement; cfg.keepBlankLines = keepBlankLines; + cfg.endWithNewline = endWithNewline; cfg.nrOfIndentSpace = nrOfIndentSpace; cfg.indentBlankLines = indentBlankLines; cfg.indentSchemaLocation = indentSchemaLocation; + cfg.indentAttribute = indentAttribute; cfg.predefinedSortOrder = predefinedSortOrder; + cfg.quiet = quiet; cfg.sortOrderFile = sortOrderFile; cfg.sortDependencies = sortDependencies; + cfg.sortDependencyManagement = sortDependencyManagement; cfg.sortDependencyExclusions = sortDependencyExclusions; cfg.sortPlugins = sortPlugins; cfg.sortProperties = sortProperties; diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Buf.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Buf.java new file mode 100644 index 0000000000..1d336d7639 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Buf.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2024 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.protobuf; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.protobuf.BufStep; + +public class Buf implements FormatterStepFactory { + + @Parameter + private String version; + + @Parameter + private String pathToExe; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + BufStep buf = BufStep.withVersion(version == null ? BufStep.defaultVersion() : version); + + if (pathToExe != null) { + buf = buf.withPathToExe(pathToExe); + } + + return buf.create(); + } + +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Protobuf.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Protobuf.java new file mode 100644 index 0000000000..78d60f4bde --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/protobuf/Protobuf.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2025 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.protobuf; + +import static com.diffplug.spotless.protobuf.ProtobufConstants.LICENSE_HEADER_DELIMITER; + +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.common.collect.ImmutableSet; +import com.diffplug.spotless.maven.FormatterFactory; +import com.diffplug.spotless.maven.cpp.Clang; +import com.diffplug.spotless.maven.generic.LicenseHeader; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} + * configuration element. + *

+ * It defines a formatter for protobuf source files that can execute both language agnostic (e.g. + * {@link LicenseHeader}) and protobuf-specific (e.g. {@link Buf}) steps. + */ +public class Protobuf extends FormatterFactory { + + private static final Set DEFAULT_INCLUDES = ImmutableSet.of("**/*.proto"); + + @Override + public Set defaultIncludes(MavenProject project) { + return DEFAULT_INCLUDES; + } + + @Override + public String licenseHeaderDelimiter() { + return LICENSE_HEADER_DELIMITER; + } + + public void addBuf(Buf buf) { + addStepFactory(buf); + } + + public void addClang(Clang clang) { + addStepFactory(clang); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Python.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Python.java index 09443f070b..df7348c16c 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Python.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/python/Python.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -30,7 +32,7 @@ public class Python extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/Rdf.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/Rdf.java new file mode 100644 index 0000000000..d92e09aa58 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/Rdf.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.rdf; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +public class Rdf extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addFormat(RdfFormat rdfFormat) { + addStepFactory(rdfFormat); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/RdfFormat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/RdfFormat.java new file mode 100644 index 0000000000..67d865866c --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rdf/RdfFormat.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 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.rdf; + +import java.util.Map; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.rdf.RdfFormatterConfig; +import com.diffplug.spotless.rdf.RdfFormatterStep; + +public class RdfFormat implements FormatterStepFactory { + @Parameter + String turtleFormatterVersion = RdfFormatterStep.LATEST_TURTLE_FORMATTER_VERSION; + + @Parameter + boolean failOnWarning = true; + + @Parameter + boolean verify = true; + + @Parameter + Map turtle; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + RdfFormatterConfig formatterConfig = RdfFormatterConfig + .builder() + .failOnWarning(failOnWarning) + .turtleFormatterVersion(turtleFormatterVersion) + .verify(verify) + .build(); + try { + return RdfFormatterStep.create(formatterConfig, + turtle, config.getProvisioner()); + } catch (Exception e) { + throw new RuntimeException("Error creating RDF formatter step", e); + } + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scala.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scala.java index 423ca71930..116d89263c 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scala.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scala.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,10 @@ import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.common.collect.ImmutableSet; +import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.maven.FormatterFactory; import com.diffplug.spotless.maven.generic.LicenseHeader; @@ -31,10 +34,10 @@ public class Scala extends FormatterFactory { private static final Set DEFAULT_INCLUDES = ImmutableSet.of("src/main/scala/**/*.scala", "src/test/scala/**/*.scala", "src/main/scala/**/*.sc", "src/test/scala/**/*.sc"); - private static final String LICENSE_HEADER_DELIMITER = "package "; + private static final String LICENSE_HEADER_DELIMITER = LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return DEFAULT_INCLUDES; } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scalafmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scalafmt.java index 470a5ddb72..e0dc44bef5 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scalafmt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/scala/Scalafmt.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,10 +32,14 @@ public class Scalafmt implements FormatterStepFactory { @Parameter private String version; + @Parameter + private String scalaMajorVersion; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String scalafmtVersion = version != null ? version : ScalaFmtStep.defaultVersion(); + String scalafmtScalaMajorVersion = scalaMajorVersion != null ? scalaMajorVersion : ScalaFmtStep.defaultScalaMajorVersion(); File configFile = config.getFileLocator().locateFile(file); - return ScalaFmtStep.create(scalafmtVersion, config.getProvisioner(), configFile); + return ScalaFmtStep.create(scalafmtVersion, scalafmtScalaMajorVersion, config.getProvisioner(), configFile); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java new file mode 100644 index 0000000000..0626c5ceb9 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shell.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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.shell; + +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; +import com.diffplug.spotless.maven.generic.LicenseHeader; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + *

+ * It defines a formatter for shell source files that can execute both language agnostic (e.g. {@link LicenseHeader}) + * and shell-specific (e.g. {@link Shfmt}) steps. + */ +public class Shell extends FormatterFactory { + private static final Set DEFAULT_INCLUDES = Set.of("**/*.sh"); + + @Override + public Set defaultIncludes(MavenProject project) { + return DEFAULT_INCLUDES; + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addShfmt(Shfmt shfmt) { + addStepFactory(shfmt); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java new file mode 100644 index 0000000000..4bab77ecd6 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/shell/Shfmt.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.shell; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.shell.ShfmtStep; + +public class Shfmt implements FormatterStepFactory { + @Parameter + private String version; + + @Parameter + private String pathToExe; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + ShfmtStep shfmt = ShfmtStep.withVersion(version == null ? ShfmtStep.defaultVersion() : version); + if (pathToExe != null) { + shfmt = shfmt.withPathToExe(pathToExe); + } + return shfmt.create(); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql/Sql.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql/Sql.java index c49ac074d9..64ebcb55d7 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql/Sql.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/sql/Sql.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.maven.FormatterFactory; /** @@ -27,7 +29,7 @@ */ public class Sql extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/BiomeTs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/BiomeTs.java new file mode 100644 index 0000000000..e9f84d9edb --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/BiomeTs.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2024 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.typescript; + +import com.diffplug.spotless.biome.BiomeFlavor; +import com.diffplug.spotless.maven.generic.AbstractBiome; + +/** + * Biome formatter step for TypeScript. + */ +public class BiomeTs extends AbstractBiome { + public BiomeTs() { + super(BiomeFlavor.BIOME); + } + + @Override + protected String getLanguage() { + return "ts?"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/EslintTs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/EslintTs.java new file mode 100644 index 0000000000..dc43185dcd --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/EslintTs.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022-2023 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.typescript; + +import java.util.Map; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.javascript.AbstractEslint; +import com.diffplug.spotless.npm.EslintConfig; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintTypescriptConfig; + +public class EslintTs extends AbstractEslint { + + @Parameter + private String tsconfigFile; + + @Override + protected EslintConfig eslintConfig(FormatterStepConfig stepConfig) { + return new EslintTypescriptConfig( + configFile != null ? stepConfig.getFileLocator().locateFile(configFile) : null, + configJs, + tsconfigFile != null ? stepConfig.getFileLocator().locateFile(tsconfigFile) : null); + } + + @Override + protected Map createDefaultDependencies() { + return this.eslintVersion == null ? EslintFormatterStep.defaultDevDependenciesForTypescript() : EslintFormatterStep.defaultDevDependenciesTypescriptWithEslint(this.eslintVersion); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Tsfmt.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Tsfmt.java index 9ebb9ac503..396c688635 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Tsfmt.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Tsfmt.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.maven.FormatterStepConfig; -import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.maven.npm.AbstractNpmFormatterStepFactory; import com.diffplug.spotless.npm.NpmPathResolver; import com.diffplug.spotless.npm.TsConfigFileType; import com.diffplug.spotless.npm.TsFmtFormatterStep; import com.diffplug.spotless.npm.TypedTsFmtConfigFile; -public class Tsfmt implements FormatterStepFactory { +public class Tsfmt extends AbstractNpmFormatterStepFactory { @Parameter private String tslintFile; @@ -52,12 +52,6 @@ public class Tsfmt implements FormatterStepFactory { @Parameter private String tslintVersion; - @Parameter - private String npmExecutable; - - @Parameter - private String npmrc; - @Parameter private Map config; @@ -74,10 +68,6 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { devDependencies.put("tslint", tslintVersion); } - File npm = npmExecutable != null ? stepConfig.getFileLocator().locateFile(npmExecutable) : null; - - File npmrcFile = npmrc != null ? stepConfig.getFileLocator().locateFile(npmrc) : null; - TypedTsFmtConfigFile configFile; Map configInline; // check that there is only 1 config file or inline config @@ -119,9 +109,11 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { throw onlyOneConfig(); } - File buildDir = stepConfig.getFileLocator().getBuildDir(); - NpmPathResolver npmPathResolver = new NpmPathResolver(npm, npmrcFile, stepConfig.getFileLocator().getBaseDir()); - return TsFmtFormatterStep.create(devDependencies, stepConfig.getProvisioner(), buildDir, npmPathResolver, configFile, configInline); + File buildDir = buildDir(stepConfig); + File baseDir = baseDir(stepConfig); + File cacheDir = cacheDir(stepConfig); + NpmPathResolver npmPathResolver = npmPathResolver(stepConfig); + return TsFmtFormatterStep.create(devDependencies, stepConfig.getProvisioner(), baseDir, buildDir, cacheDir, npmPathResolver, configFile, configInline); } private static IllegalArgumentException onlyOneConfig() { diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java index da6e6ddd91..2646ca05b0 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,16 +18,18 @@ import java.util.Collections; import java.util.Set; +import org.apache.maven.project.MavenProject; + import com.diffplug.spotless.maven.FormatterFactory; /** * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. *

- * It defines a formatter for typescript source files. + * It defines formatters for typescript source files. */ public class Typescript extends FormatterFactory { @Override - public Set defaultIncludes() { + public Set defaultIncludes(MavenProject project) { return Collections.emptySet(); } @@ -39,4 +41,12 @@ public String licenseHeaderDelimiter() { public void addTsfmt(Tsfmt tsfmt) { addStepFactory(tsfmt); } + + public void addEslint(EslintTs eslint) { + addStepFactory(eslint); + } + + public void addBiome(BiomeTs biome) { + addStepFactory(biome); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java new file mode 100644 index 0000000000..d97cc41647 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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.yaml; + +import java.util.Collections; +import java.util.Map; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterFactory; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.yaml.JacksonYamlConfig; +import com.diffplug.spotless.yaml.JacksonYamlStep; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class JacksonYaml implements FormatterStepFactory { + + @Parameter + private String version = JacksonYamlStep.defaultVersion(); + + @Parameter + private Map features = Collections.emptyMap(); + + @Parameter + private Map yamlFeatures = Collections.emptyMap(); + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { + JacksonYamlConfig jacksonConfig = new JacksonYamlConfig(); + + jacksonConfig.appendFeatureToToggle(features); + jacksonConfig.appendYamlFeatureToToggle(yamlFeatures); + + return JacksonYamlStep + .create(jacksonConfig, version, stepConfig.getProvisioner()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java new file mode 100644 index 0000000000..e22f6d40ce --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.yaml; + +import java.util.Collections; +import java.util.Set; + +import org.apache.maven.project.MavenProject; + +import com.diffplug.spotless.maven.FormatterFactory; + +/** + * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element. + */ +public class Yaml extends FormatterFactory { + @Override + public Set defaultIncludes(MavenProject project) { + return Collections.emptySet(); + } + + @Override + public String licenseHeaderDelimiter() { + return null; + } + + public void addJackson(JacksonYaml jackson) { + addStepFactory(jackson); + } + +} diff --git a/plugin-maven/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/plugin-maven/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml new file mode 100644 index 0000000000..9000b83704 --- /dev/null +++ b/plugin-maven/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml @@ -0,0 +1,32 @@ + + + + + + + + apply + check + + + + + true + false + + + + + \ No newline at end of file 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 7d2b26ee89..c1cb0f78d7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ import org.junit.jupiter.api.Test; import com.diffplug.common.base.StringPrinter; +import com.diffplug.spotless.ClearGitConfig; +@ClearGitConfig class GitRatchetMavenTest extends MavenIntegrationHarness { private static final String TEST_PATH = "src/markdown/test.md"; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/IdeHookTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/IdeHookTest.java new file mode 100644 index 0000000000..3c9a1a17d6 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/IdeHookTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2023 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; + +import java.io.File; +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.ProcessRunner; + +class IdeHookTest extends MavenIntegrationHarness { + private String output, error; + private File dirty, clean, diverge, outofbounds; + + @BeforeEach + void before() throws IOException { + writePomWithFormatSteps("\n" + + " DIRTY.md\n" + + " CLEAN.md\n" + + " \n" + + " \n" + + " Greetings to Mars\n" + + " World\n" + + " Mars\n" + + " "); + + dirty = setFile("DIRTY.md").toContent("World"); + clean = setFile("CLEAN.md").toContent("Mars"); + outofbounds = setFile("OUTOFBOUNDS.md").toContent("Mars"); + ; + } + + private void runWith(String... arguments) throws IOException, InterruptedException { + ProcessRunner.Result result = mavenRunner() + .withArguments(arguments) + .runNoError(); + + this.output = result.stdOutUtf8(); + this.error = result.stdErrUtf8(); + } + + @Test + void dirty() throws IOException, InterruptedException { + runWith("spotless:apply", "--quiet", "-DspotlessIdeHook=\"" + dirty.getAbsolutePath() + "\"", "-DspotlessIdeHookUseStdOut=true"); + Assertions.assertThat(output).isEqualTo("Mars"); + Assertions.assertThat(error).startsWith("IS DIRTY"); + } + + @Test + void clean() throws IOException, InterruptedException { + runWith("spotless:apply", "--quiet", "-DspotlessIdeHook=" + clean.getAbsolutePath(), "-DspotlessIdeHookUseStdOut=true"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).startsWith("IS CLEAN"); + } + + @Test + void outofbounds() throws IOException, InterruptedException { + runWith("spotless:apply", "--quiet", "-DspotlessIdeHook=" + outofbounds.getAbsolutePath(), "-DspotlessIdeHookUseStdOut=true"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).isEmpty(); + } + + @Test + void notAbsolute() throws IOException, InterruptedException { + runWith("spotless:apply", "--quiet", "-DspotlessIdeHook=\"pom.xml\"", "-DspotlessIdeHookUseStdOut=true"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path"); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index 939940a315..0d0582e180 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,7 @@ package com.diffplug.spotless.maven; import static com.diffplug.common.base.Strings.isNullOrEmpty; -import static java.util.Arrays.asList; import static java.util.Arrays.stream; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.fail; @@ -33,6 +31,8 @@ import java.nio.file.Path; import java.util.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import com.github.mustachejava.DefaultMustacheFactory; @@ -42,6 +42,7 @@ import com.diffplug.common.base.Unhandled; import com.diffplug.common.io.Resources; import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.ProcessRunner; import com.diffplug.spotless.ResourceHarness; public class MavenIntegrationHarness extends ResourceHarness { @@ -53,14 +54,15 @@ public class MavenIntegrationHarness extends ResourceHarness { */ private static final String SPOTLESS_MAVEN_VERSION_IDE = null; - private static final String LOCAL_MAVEN_REPOSITORY_DIR = "localMavenRepositoryDir"; + private static final String POM_TEMPLATE = "/pom-test.xml.mustache"; private static final String SPOTLESS_MAVEN_PLUGIN_VERSION = "spotlessMavenPluginVersion"; + private static final String BUILD = "build"; private static final String CONFIGURATION = "configuration"; private static final String EXECUTIONS = "executions"; private static final String MODULES = "modules"; private static final String DEPENDENCIES = "dependencies"; + private static final String PLUGINS = "plugins"; private static final String MODULE_NAME = "name"; - private static final String CHILD_ID = "childId"; private static final int REMOTE_DEBUG_PORT = 5005; private final MustacheFactory mustacheFactory = new DefaultMustacheFactory(); @@ -68,12 +70,12 @@ public class MavenIntegrationHarness extends ResourceHarness { /** * Each test gets its own temp folder, and we create a maven * build there and run it. - * + *

* Because those test folders don't have a .gitattributes file, * git on windows will default to \r\n. So now if you read a * test file from the spotless test resources, and compare it * to a build result, the line endings won't match. - * + *

* By sticking this .gitattributes file into the test directory, * we ensure that the default Spotless line endings policy of * GIT_ATTRIBUTES will use \n, so that tests match the test @@ -136,8 +138,12 @@ protected void writePomWithCppSteps(String... steps) throws IOException { writePom(groupWithSteps("cpp", steps)); } - protected void writePomWithTypescriptSteps(String... steps) throws IOException { - writePom(groupWithSteps("typescript", including("**/*.ts"), steps)); + protected void writePomWithJavascriptSteps(String includes, String... steps) throws IOException { + writePom(groupWithSteps("javascript", including(includes), steps)); + } + + protected void writePomWithTypescriptSteps(String includes, String... steps) throws IOException { + writePom(groupWithSteps("typescript", including(includes), steps)); } protected void writePomWithSqlSteps(String... steps) throws IOException { @@ -148,27 +154,80 @@ protected void writePomWithPrettierSteps(String includes, String... steps) throw writePom(formats(groupWithSteps("format", including(includes), steps))); } + protected void writePomWithBiomeSteps(String includes, String... steps) throws IOException { + writePom(formats(groupWithSteps("format", including(includes), steps))); + } + + @Deprecated + protected void writePomWithRomeSteps(String includes, String... steps) throws IOException { + writePom(formats(groupWithSteps("format", including(includes), steps))); + } + + protected void writePomWithPrettierSteps(String[] plugins, String includes, String... steps) throws IOException { + writePom(null, formats(groupWithSteps("format", including(includes), steps)), null, plugins); + } + protected void writePomWithPomSteps(String... steps) throws IOException { writePom(groupWithSteps("pom", including("pom_test.xml"), steps)); } + protected void writePomWithProtobufSteps(String... steps) throws IOException { + writePom(groupWithSteps("protobuf", steps)); + } + protected void writePomWithMarkdownSteps(String... steps) throws IOException { writePom(groupWithSteps("markdown", including("**/*.md"), steps)); } + protected void writePomWithJsonSteps(String... steps) throws IOException { + writePom(groupWithSteps("json", including("**/*.json"), steps)); + } + + protected void writePomWithCssSteps(String... steps) throws IOException { + writePom(groupWithSteps("css", including("**/*.css"), steps)); + } + + protected void writePomWithShellSteps(String... steps) throws IOException { + writePom(groupWithSteps("shell", including("**/*.sh"), steps)); + } + + protected void writePomWithYamlSteps(String... steps) throws IOException { + writePom(groupWithSteps("yaml", including("**/*.yaml"), steps)); + } + + protected void writePomWithGherkinSteps(String... steps) throws IOException { + writePom(groupWithSteps("gherkin", including("**/*.feature"), steps)); + } + + protected void writePomWithGoSteps(String... steps) throws IOException { + writePom(groupWithSteps("go", including("**/*.go"), steps)); + } + protected void writePom(String... configuration) throws IOException { - writePom(null, configuration, null); + writePom(null, configuration, null, null); } - protected void writePom(String[] executions, String[] configuration, String[] dependencies) throws IOException { - String pomXmlContent = createPomXmlContent(null, executions, configuration, dependencies); + protected void writePom(String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException { + String pomXmlContent = createPomXmlContent(null, executions, configuration, dependencies, plugins); setFile("pom.xml").toContent(pomXmlContent); } protected MavenRunner mavenRunner() throws IOException { return MavenRunner.create() .withProjectDir(rootFolder()) - .withLocalRepository(new File(getSystemProperty(LOCAL_MAVEN_REPOSITORY_DIR))); + .withRunner(runner); + } + + private static ProcessRunner runner; + + @BeforeAll + static void setupRunner() throws IOException { + runner = new ProcessRunner(); + } + + @AfterAll + static void closeRunner() throws IOException { + runner.close(); } /** @@ -180,24 +239,25 @@ protected MavenRunner mavenRunnerWithRemoteDebug() throws IOException { return mavenRunner().withRemoteDebug(REMOTE_DEBUG_PORT); } - protected MultiModuleProjectCreator multiModuleProject() { - return new MultiModuleProjectCreator(); - } - - protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration, String[] dependencies) throws IOException { - return createPomXmlContent("/pom-test.xml.mustache", pluginVersion, executions, configuration, dependencies); + protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException { + return createPomXmlContent(POM_TEMPLATE, pluginVersion, executions, configuration, dependencies, plugins); } - protected String createPomXmlContent(String pomTemplate, String pluginVersion, String[] executions, String[] configuration, String[] dependencies) throws IOException { - Map params = buildPomXmlParams(pluginVersion, executions, configuration, null, dependencies); + protected String createPomXmlContent(String pomTemplate, String pluginVersion, String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException { + Map params = buildPomXmlParams(pluginVersion, null, executions, configuration, null, dependencies, plugins); return createPomXmlContent(pomTemplate, params); } protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration) throws IOException { - return createPomXmlContent(pluginVersion, executions, configuration, null); + return createPomXmlContent(pluginVersion, executions, configuration, null, null); + } + + protected String createPomXmlContent(String[] build, String[] configuration) throws IOException { + Map params = buildPomXmlParams(null, build, null, configuration, null, null, null); + return createPomXmlContent(POM_TEMPLATE, params); } - private String createPomXmlContent(String pomTemplate, Map params) throws IOException { + protected String createPomXmlContent(String pomTemplate, Map params) throws IOException { URL url = MavenIntegrationHarness.class.getResource(pomTemplate); try (BufferedReader reader = Resources.asCharSource(url, StandardCharsets.UTF_8).openBufferedStream()) { Mustache mustache = mustacheFactory.compile(reader, "pom"); @@ -207,10 +267,14 @@ private String createPomXmlContent(String pomTemplate, Map param } } - private static Map buildPomXmlParams(String pluginVersion, String[] executions, String[] configuration, String[] modules, String[] dependencies) { + protected static Map buildPomXmlParams(String pluginVersion, String[] build, String[] executions, String[] configuration, String[] modules, String[] dependencies, String[] plugins) { Map params = new HashMap<>(); params.put(SPOTLESS_MAVEN_PLUGIN_VERSION, pluginVersion == null ? getSystemProperty(SPOTLESS_MAVEN_PLUGIN_VERSION) : pluginVersion); + if (build != null) { + params.put(BUILD, String.join("\n", build)); + } + if (configuration != null) { params.put(CONFIGURATION, String.join("\n", configuration)); } @@ -228,6 +292,10 @@ private static Map buildPomXmlParams(String pluginVersion, Strin params.put(DEPENDENCIES, String.join("\n", dependencies)); } + if (plugins != null) { + params.put(PLUGINS, String.join("\n", plugins)); + } + return params; } @@ -235,8 +303,6 @@ private static String getSystemProperty(String name) { if (SPOTLESS_MAVEN_VERSION_IDE != null) { if (name.equals("spotlessMavenPluginVersion")) { return SPOTLESS_MAVEN_VERSION_IDE; - } else if (name.equals("localMavenRepositoryDir")) { - return new File("build/localMavenRepository").getAbsolutePath(); } else { throw Unhandled.stringException(name); } @@ -248,7 +314,7 @@ private static String getSystemProperty(String name) { return value; } - private static String[] groupWithSteps(String group, String[] includes, String... steps) { + protected static String[] groupWithSteps(String group, String[] includes, String... steps) { String[] result = new String[steps.length + includes.length + 2]; result[0] = "<" + group + ">"; System.arraycopy(includes, 0, result, 1, includes.length); @@ -257,97 +323,22 @@ private static String[] groupWithSteps(String group, String[] includes, String.. return result; } - private static String[] groupWithSteps(String group, String... steps) { + protected static String[] groupWithSteps(String group, String... steps) { return groupWithSteps(group, new String[]{}, steps); } - private static String[] including(String... includes) { + protected static String[] including(String... includes) { return groupWithSteps("includes", groupWithSteps("include", includes)); } - private static String[] formats(String... formats) { + protected static String[] formats(String... formats) { return groupWithSteps("formats", formats); } - protected class MultiModuleProjectCreator { - - private String configSubProject; - private SubProjectFile[] configSubProjectFiles; - private String[] configuration; - private final Map> subProjects = new LinkedHashMap<>(); - - protected MultiModuleProjectCreator withConfigSubProject(String name, SubProjectFile... files) { - configSubProject = name; - configSubProjectFiles = files; - return this; - } - - protected MultiModuleProjectCreator withConfiguration(String... lines) { - configuration = lines; - return this; - } - - protected MultiModuleProjectCreator addSubProject(String name, SubProjectFile... files) { - subProjects.put(name, asList(files)); - return this; - } - - protected void create() throws IOException { - createRootPom(); - createConfigSubProject(); - createSubProjects(); - } - - private void createRootPom() throws IOException { - List modulesList = new ArrayList<>(); - modulesList.add(configSubProject); - modulesList.addAll(subProjects.keySet()); - String[] modules = modulesList.toArray(new String[0]); - - Map rootPomParams = buildPomXmlParams(null, null, configuration, modules, null); - setFile("pom.xml").toContent(createPomXmlContent("/multi-module/pom-parent.xml.mustache", rootPomParams)); - } - - private void createConfigSubProject() throws IOException { - if (configSubProject != null) { - String content = createPomXmlContent("/multi-module/pom-config.xml.mustache", emptyMap()); - setFile(configSubProject + "/pom.xml").toContent(content); - - createSubProjectFiles(configSubProject, asList(configSubProjectFiles)); - } - } - - private void createSubProjects() throws IOException { - for (Map.Entry> entry : subProjects.entrySet()) { - String subProjectName = entry.getKey(); - List subProjectFiles = entry.getValue(); - - String content = createPomXmlContent("/multi-module/pom-child.xml.mustache", singletonMap(CHILD_ID, subProjectName)); - setFile(subProjectName + "/pom.xml").toContent(content); - - createSubProjectFiles(subProjectName, subProjectFiles); - } - } - - private void createSubProjectFiles(String subProjectName, List subProjectFiles) throws IOException { - for (SubProjectFile file : subProjectFiles) { - setFile(subProjectName + '/' + file.to).toResource(file.from); - } - } - } - - protected static class SubProjectFile { - - private final String from; - private final String to; - - private SubProjectFile(String from, String to) { - this.from = from; - this.to = to; - } - - protected static SubProjectFile file(String from, String to) { - return new SubProjectFile(from, to); - } + protected static String[] formats(String[]... formats) { + String[] formatsArray = Arrays.stream(formats) + .flatMap(Arrays::stream) + .toArray(String[]::new); + return formats(formatsArray); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenProvisionerTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenProvisionerTest.java index 3e769d7705..e1214a0d59 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenProvisionerTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenProvisionerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 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 @@ class MavenProvisionerTest extends MavenIntegrationHarness { void testMultipleDependenciesExcludingTransitives() throws Exception { writePomWithJavaSteps( "", - " 4.8.0", + " 4.9", ""); setFile("formatter.xml").toResource("java/eclipse/formatter.xml"); assertResolveDependenciesWorks(); @@ -33,7 +33,6 @@ void testMultipleDependenciesExcludingTransitives() throws Exception { void testSingleDependencyIncludingTransitives() throws Exception { writePomWithJavaSteps( "", - " 1.2", ""); assertResolveDependenciesWorks(); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenRunner.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenRunner.java index a86e000187..c8d845f8a2 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenRunner.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,19 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Objects; -import com.diffplug.common.base.Throwables; -import com.diffplug.common.io.ByteStreams; -import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.ProcessRunner; /** - * Harness for running a maven build, same idea as the - * GradleRunner from the gradle testkit. + * Harness for running a Maven build, same idea as the + * GradleRunner from the Gradle testkit. */ public class MavenRunner { public static MavenRunner create() { @@ -46,8 +40,8 @@ private MavenRunner() {} private File projectDir; private String[] args; - private File localRepositoryDir; private Map environment = new HashMap<>(); + private ProcessRunner runner; public MavenRunner withProjectDir(File projectDir) { this.projectDir = Objects.requireNonNull(projectDir); @@ -59,8 +53,8 @@ public MavenRunner withArguments(String... args) { return this; } - public MavenRunner withLocalRepository(File localRepositoryDir) { - this.localRepositoryDir = localRepositoryDir; + public MavenRunner withRunner(ProcessRunner runner) { + this.runner = runner; return this; } @@ -70,104 +64,25 @@ public MavenRunner withRemoteDebug(int port) { return this; } - private Result run() throws IOException, InterruptedException { + private ProcessRunner.Result run() throws IOException, InterruptedException { Objects.requireNonNull(projectDir, "Need to call withProjectDir() first"); Objects.requireNonNull(args, "Need to call withArguments() first"); - // run maven with the given args in the given directory - String argsString = String.join(" ", Arrays.asList(args)); - List cmds = getPlatformCmds("-e -Dmaven.repo.local=" + localRepositoryDir + ' ' + argsString); - ProcessBuilder builder = new ProcessBuilder(cmds); - builder.directory(projectDir); - builder.environment().putAll(environment); - Process process = builder.start(); - // slurp and return the stdout, stderr, and exitValue - Slurper output = new Slurper(process.getInputStream()); - Slurper error = new Slurper(process.getErrorStream()); - int exitValue = process.waitFor(); - output.join(); - error.join(); - return new Result(exitValue, output.result(), error.result()); + // run Maven with the given args in the given directory + String argsString = "-e " + String.join(" ", Arrays.asList(args)); + return runner.shellWinUnix(projectDir, environment, "mvnw " + argsString, "./mvnw " + argsString); } /** Runs the command and asserts that exit code is 0. */ - public Result runNoError() throws IOException, InterruptedException { - Result result = run(); - assertThat(result.exitValue()).as("Run without error %s", result).isEqualTo(0); + public ProcessRunner.Result runNoError() throws IOException, InterruptedException { + ProcessRunner.Result result = run(); + assertThat(result.exitCode()).as("Run without error %s", result).isEqualTo(0); return result; } /** Runs the command and asserts that exit code is not 0. */ - public Result runHasError() throws IOException, InterruptedException { - Result result = run(); - assertThat(result.exitValue()).as("Run with error %s", result).isNotEqualTo(0); + public ProcessRunner.Result runHasError() throws IOException, InterruptedException { + ProcessRunner.Result result = run(); + assertThat(result.exitCode()).as("Run with error %s", result).isNotEqualTo(0); return result; } - - public static class Result { - private final int exitValue; - private final String output; - private final String error; - - public Result(int exitValue, String output, String error) { - super(); - this.exitValue = exitValue; - this.output = Objects.requireNonNull(output); - this.error = Objects.requireNonNull(error); - } - - public int exitValue() { - return exitValue; - } - - public String output() { - return output; - } - - public String error() { - return error; - } - - @Override - public String toString() { - return "Result{" + - "exitValue=" + exitValue + - ", output='" + output + '\'' + - ", error='" + error + '\'' + - '}'; - } - } - - /** Prepends any arguments necessary to run a console command. */ - private static List getPlatformCmds(String cmd) { - if (FileSignature.machineIsWin()) { - return Arrays.asList("cmd", "/c", "mvnw " + cmd); - } else { - return Arrays.asList("/bin/sh", "-c", "./mvnw " + cmd); - } - } - - private static class Slurper extends Thread { - private final InputStream input; - private volatile String result; - - Slurper(InputStream input) { - this.input = Objects.requireNonNull(input); - start(); - } - - @Override - public void run() { - try { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - ByteStreams.copy(input, output); - result = output.toString(Charset.defaultCharset().name()); - } catch (Exception e) { - result = Throwables.getStackTraceAsString(e); - } - } - - public String result() { - return result; - } - } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java index e67b1bcad3..a2d491cb2b 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,15 @@ */ package com.diffplug.spotless.maven; -import static com.diffplug.spotless.maven.MavenIntegrationHarness.SubProjectFile.file; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -24,31 +32,31 @@ class MultiModuleProjectTest extends MavenIntegrationHarness { @Test void testConfigurationDependency() throws Exception { /* - create a multi-module project with the following stucture: + create a multi-module project with the following structure: /junit-tmp-dir ├── config - │   ├── pom.xml - │   └── src/main/resources/configs - │   ├── eclipse-formatter.xml - │   └── scalafmt.conf + │ ├── pom.xml + │ └── src/main/resources/configs + │ ├── eclipse-formatter.xml + │ └── scalafmt.conf ├── mvnw ├── mvnw.cmd ├── one - │   ├── pom.xml - │   └── src - │   ├── main/java/test1.java - │   └── test/java/test2.java + │ ├── pom.xml + │ └── src + │ ├── main/java/test1.java + │ └── test/java/test2.java ├── two - │   ├── pom.xml - │   └── src - │   ├── main/java/test1.java - │   └── test/java/test2.java + │ ├── pom.xml + │ └── src + │ ├── main/java/test1.java + │ └── test/java/test2.java ├── three - │   ├── pom.xml - │   └── src - │   ├── main/scala/test1.scala - │   └── test/scala/test2.scala + │ ├── pom.xml + │ └── src + │ ├── main/scala/test1.scala + │ └── test/scala/test2.scala ├── pom.xml ├── .mvn ├── mvnw @@ -62,7 +70,7 @@ void testConfigurationDependency() throws Exception { "", " ", " configs/eclipse-formatter.xml", - " 4.7.1", + " 4.9", " ", "", "", @@ -96,4 +104,91 @@ void testConfigurationDependency() throws Exception { assertFile("three/src/main/scala/test1.scala").sameAsResource("scala/scalafmt/basic.cleanWithCustomConf_3.0.0"); assertFile("three/src/test/scala/test2.scala").sameAsResource("scala/scalafmt/basic.cleanWithCustomConf_3.0.0"); } + + private static final String CHILD_ID = "childId"; + + protected MultiModuleProjectCreator multiModuleProject() { + return new MultiModuleProjectCreator(); + } + + class MultiModuleProjectCreator { + private String configSubProject; + private SubProjectFile[] configSubProjectFiles; + private String[] configuration; + private final Map> subProjects = new LinkedHashMap<>(); + + protected MultiModuleProjectCreator withConfigSubProject(String name, SubProjectFile... files) { + configSubProject = name; + configSubProjectFiles = files; + return this; + } + + protected MultiModuleProjectCreator withConfiguration(String... lines) { + configuration = lines; + return this; + } + + protected MultiModuleProjectCreator addSubProject(String name, SubProjectFile... files) { + subProjects.put(name, asList(files)); + return this; + } + + protected void create() throws IOException { + createRootPom(); + createConfigSubProject(); + createSubProjects(); + } + + private void createRootPom() throws IOException { + List modulesList = new ArrayList<>(); + modulesList.add(configSubProject); + modulesList.addAll(subProjects.keySet()); + String[] modules = modulesList.toArray(new String[0]); + + Map rootPomParams = buildPomXmlParams(null, null, null, configuration, modules, null, null); + setFile("pom.xml").toContent(createPomXmlContent("/multi-module/pom-parent.xml.mustache", rootPomParams)); + } + + private void createConfigSubProject() throws IOException { + if (configSubProject != null) { + String content = createPomXmlContent("/multi-module/pom-config.xml.mustache", emptyMap()); + setFile(configSubProject + "/pom.xml").toContent(content); + + createSubProjectFiles(configSubProject, asList(configSubProjectFiles)); + } + } + + private void createSubProjects() throws IOException { + for (Map.Entry> entry : subProjects.entrySet()) { + String subProjectName = entry.getKey(); + List subProjectFiles = entry.getValue(); + + String content = createPomXmlContent("/multi-module/pom-child.xml.mustache", singletonMap(CHILD_ID, subProjectName)); + setFile(subProjectName + "/pom.xml").toContent(content); + + createSubProjectFiles(subProjectName, subProjectFiles); + } + } + + private void createSubProjectFiles(String subProjectName, List subProjectFiles) { + for (SubProjectFile file : subProjectFiles) { + setFile(subProjectName + '/' + file.to).toResource(file.from); + } + } + } + + static class SubProjectFile { + + private final String from; + private final String to; + + private SubProjectFile(String from, String to) { + this.from = from; + this.to = to; + } + } + + static SubProjectFile file(String from, String to) { + return new SubProjectFile(from, to); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpecificFilesTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpecificFilesTest.java index 55cd586582..6ce11d179f 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpecificFilesTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpecificFilesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,6 @@ private void integration(String patterns, boolean firstFormatted, boolean second " src/**/java/**/*.java", "", "", - " 1.2", ""); setFile(testFile(1)).toResource(fixture(false)); 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 cb37e4a4c5..92a78e3923 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-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ProcessRunner; + class SpotlessCheckMojoTest extends MavenIntegrationHarness { private static final String UNFORMATTED_FILE = "license/MissingLicense.test"; @@ -60,6 +62,7 @@ void testSpotlessCheckBindingToVerifyPhase() throws Exception { " ${basedir}/license.txt", " ", ""}, + null, null); testSpotlessCheck(UNFORMATTED_FILE, "verify", true); @@ -72,8 +75,8 @@ private void testSpotlessCheck(String fileName, String command, boolean expectEr MavenRunner mavenRunner = mavenRunner().withArguments(command); if (expectError) { - MavenRunner.Result result = mavenRunner.runHasError(); - assertThat(result.output()).contains("The following files had format violations"); + ProcessRunner.Result result = mavenRunner.runHasError(); + assertThat(result.stdOutUtf8()).contains("The following files had format violations"); } else { mavenRunner.runNoError(); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java new file mode 100644 index 0000000000..5bb7424ef9 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2023-2024 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.biome; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.owasp.encoder.Encode.forXml; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +/** + * Tests for the Biome formatter used via the Maven spotless plugin. + */ +class BiomeMavenTest extends MavenIntegrationHarness { + /** + * Tests that biome can be used as a CSS formatting step, using biome 1.8.3 + * which requires opt-in. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asCssStepExperimental() throws Exception { + writePomWithCssSteps("**/*.css", "1.8.3configs"); + setFile("biome_test.css").toResource("biome/css/fileBefore.css"); + setFile("configs/biome.json").toResource("biome/config/css-enabled.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.css").sameAsResource("biome/css/fileAfter.css"); + } + + /** + * Tests that biome can be used as a CSS formatting step, with biome 1.9.0 + * which does not require opt-in. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asCssStepStable() throws Exception { + writePomWithCssSteps("**/*.js", "1.9.0"); + setFile("biome_test.css").toResource("biome/css/fileBefore.css"); + var res = mavenRunner().withArguments("spotless:apply").runNoError(); + System.out.println(res.stdOutUtf8()); + assertFile("biome_test.css").sameAsResource("biome/css/fileAfter.css"); + } + + /** + * Tests that Biome can be used as a generic formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asGenericStep() throws Exception { + writePomWithBiomeSteps("**/*.js", "1.2.0"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + } + + /** + * Tests that Biome can be used as a JavaScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJavaScriptStep() throws Exception { + writePomWithJavascriptSteps("**/*.js", "1.2.0"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + } + + /** + * Tests that biome can be used as a JSON formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJsonStep() throws Exception { + writePomWithJsonSteps("**/*.json", "1.2.0"); + setFile("biome_test.json").toResource("biome/json/fileBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.json").sameAsResource("biome/json/fileAfter.json"); + } + + /** + * Tests that biome can be used as a TypeScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asTypeScriptStep() throws Exception { + writePomWithTypescriptSteps("**/*.ts", "1.2.0"); + setFile("biome_test.ts").toResource("biome/ts/fileBefore.ts"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.ts").sameAsResource("biome/ts/fileAfter.ts"); + } + + /** + * Tests that the language can be specified for the generic format step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void canSetLanguageForGenericStep() throws Exception { + writePomWithBiomeSteps("**/*.nosj", "1.2.0json"); + setFile("biome_test.nosj").toResource("biome/json/fileBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.nosj").sameAsResource("biome/json/fileAfter.json"); + } + + /** + * Tests that an absolute config path can be specified. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathAbsolute() throws Exception { + var path = newFile("configs").getAbsolutePath(); + writePomWithBiomeSteps("**/*.js", + "1.2.0" + forXml(path) + ""); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-120.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the biome.json config file can be + * specified. Uses a config file with a line width of 120. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth120() throws Exception { + writePomWithBiomeSteps("**/*.js", "1.2.0configs"); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-120.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the biome.json config file can be + * specified. Uses a config file with a line width of 80. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth80() throws Exception { + writePomWithBiomeSteps("**/*.js", "1.2.0configs"); + setFile("biome_test.js").toResource("biome/js/longLineBefore.js"); + setFile("configs/biome.json").toResource("biome/config/line-width-80.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/longLineAfter80.js"); + } + + /** + * Tests that the download directory can be an absolute path. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirAbsolute() throws Exception { + var path = newFile("target/bin/biome").getAbsoluteFile().toString(); + writePomWithBiomeSteps("**/*.js", + "1.2.0" + forXml(path) + ""); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/biome").exists() || newFile("target/bin/biome").list().length == 0); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/biome").list().length); + } + + /** + * Tests that the download directory can be changed to a path relative to the + * project's base directory. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirRelative() throws Exception { + writePomWithBiomeSteps("**/*.js", + "1.2.0target/bin/biome"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/biome").exists() || newFile("target/bin/biome").list().length == 0); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/biome").list().length); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenExeNotFound() throws Exception { + writePomWithBiomeSteps("**/*.js", "1.2.0biome/is/missing"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + var result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); + assertThat(result.stdOutUtf8()).contains("Biome executable does not exist"); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenNotParseable() throws Exception { + writePomWithBiomeSteps("**/*.js", "1.2.0json"); + setFile("biome_test.js").toResource("biome/js/fileBefore.js"); + var result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); + assertThat(result.stdOutUtf8()).contains("Format with errors is disabled."); + assertThat(result.stdOutUtf8()).contains("Unable to format file"); + assertThat(result.stdOutUtf8()).contains("Step 'biome' found problem in 'biome_test.js'"); + } + + /** + * Biome is hard-coded to ignore certain files, such as package.json. Since version 1.5.0, + * the biome CLI does not output any formatted code anymore, whereas previously it printed + * the input as-is. This tests checks that when the biome formatter outputs an empty string, + * the contents of the file to format are used instead. + * + * @throws Exception When a test failure occurs. + */ + @Test + void preservesIgnoredFiles() throws Exception { + writePomWithJsonSteps("**/*.json", "1.5.0"); + setFile("package.json").toResource("biome/json/packageBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("package.json").sameAsResource("biome/json/packageAfter.json"); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/cpp/ClangMavenIntegrationTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/cpp/ClangMavenIntegrationTest.java new file mode 100644 index 0000000000..f068cebc45 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/cpp/ClangMavenIntegrationTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024-2025 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.cpp; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.tag.ClangTest; + +@ClangTest +class ClangMavenIntegrationTest extends MavenIntegrationHarness { + + @Test + @ClangTest + void csharp() throws Exception { + writePomWithCppSteps("", "", "src/**/*.cs", "", "", + "", "", "14.0.0-1ubuntu1.1", "", ""); + setFile("src/test.cs").toResource("clang/example.cs"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/test.cs").sameAsResource("clang/example.cs.clean"); + } + + @Test + @ClangTest + void proto() throws Exception { + writePomWithCppSteps("", "", "**/*.proto", "", "", + "", "", "14.0.0-1ubuntu1.1", "", ""); + setFile("buf.proto").toResource("protobuf/buf/buf.proto"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf.proto.clean"); + } + +} 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 cb6d54a1e4..24200020f2 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-2021 DiffPlug + * Copyright 2020-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,10 @@ import org.eclipse.jgit.api.Git; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ClearGitConfig; import com.diffplug.spotless.maven.MavenIntegrationHarness; +@ClearGitConfig class LicenseHeaderRatchetTest extends MavenIntegrationHarness { private static final String NOW = String.valueOf(YearMonth.now().getYear()); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/gherkin/GherkinTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/gherkin/GherkinTest.java new file mode 100644 index 0000000000..95a60097d0 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/gherkin/GherkinTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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.gherkin; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class GherkinTest extends MavenIntegrationHarness { + @Test + public void testFormatJson_WithSimple_defaultConfig_sortByKeys() throws Exception { + writePomWithGherkinSteps(""); + + setFile("examples/main/resources/example.feature").toResource("gherkin/minimalBefore.feature"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("examples/main/resources/example.feature").sameAsResource("gherkin/minimalAfter.feature"); + } + +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/go/GofmtTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/go/GofmtTest.java new file mode 100644 index 0000000000..5cf4398f70 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/go/GofmtTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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.go; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +@com.diffplug.spotless.tag.GofmtTest +public class GofmtTest extends MavenIntegrationHarness { + @Test + void testGofmt() throws Exception { + writePomWithGoSteps("go1.21.5"); + + setFile("src/main/go/example.go").toResource("go/gofmt/go.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/go/example.go").sameAsResource("go/gofmt/go.clean"); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/GrEclipseTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/GrEclipseTest.java index 8807f47cc9..34d95dc7d2 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/GrEclipseTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/GrEclipseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ private void writePomWithGrEclipse() throws IOException { writePomWithGroovySteps( "", " ${basedir}/greclipse.properties", - " 4.19.0", + " 4.25", ""); setFile("greclipse.properties").toResource("groovy/greclipse/format/greclipse.properties"); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/RemoveSemicolonsTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/RemoveSemicolonsTest.java new file mode 100644 index 0000000000..fc78c87a27 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/groovy/RemoveSemicolonsTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 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.groovy; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class RemoveSemicolonsTest extends MavenIntegrationHarness { + + @Test + void testRemoveSemicolonsString() throws Exception { + writePomWithGroovySteps(""); + runTest("Hello World;", "Hello World"); + } + + @Test + void testNotRemoveSemicolonsString() throws Exception { + writePomWithGroovySteps(""); + runTest("Hello;World", "Hello;World"); + } + + @Test + void testRemoveSemicolons() throws Exception { + writePomWithGroovySteps(""); + + String path = "src/main/groovy/test.groovy"; + setFile(path).toResource("groovy/removeSemicolons/GroovyCodeWithSemicolons.test"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test"); + } + + private void runTest(String sourceContent, String targetContent) throws Exception { + String path = "src/main/groovy/test.groovy"; + setFile(path).toContent(sourceContent); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).hasContent(targetContent); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/FileIndexTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/FileIndexTest.java index ce9c0077e8..8cd5e8a2f7 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/FileIndexTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/FileIndexTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ void readFallsBackToEmptyIndexOnFingerprintMismatch() throws Exception { FileIndex index = FileIndex.read(config, log); assertThat(index.size()).isZero(); - verify(log).info("Fingerprint mismatch in the index file. Fallback to an empty index"); + verify(log).info("Index file corresponds to a different configuration of the plugin. Either the plugin version or its configuration has changed. Fallback to an empty index"); } @Test diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/NoopCheckerTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/NoopCheckerTest.java index 404b5e2191..4025d5b271 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/NoopCheckerTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/NoopCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2024 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,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; @@ -37,7 +36,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; @@ -116,11 +114,9 @@ private MavenProject buildMavenProject() throws IOException { private static Formatter dummyFormatter() { return Formatter.builder() - .rootDir(Paths.get("")) .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) .encoding(UTF_8) .steps(singletonList(mock(FormatterStep.class, withSettings().serializable()))) - .exceptionPolicy(new FormatExceptionPolicyStrict()) .build(); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/PluginFingerprintTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/PluginFingerprintTest.java index 90e59d657e..a0d55da86e 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/PluginFingerprintTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/PluginFingerprintTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,21 +21,24 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.ByteArrayInputStream; -import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.apache.maven.model.Model; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginManagement; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.ReaderFactory; import org.codehaus.plexus.util.xml.XmlStreamReader; import org.junit.jupiter.api.Test; -import com.diffplug.spotless.FormatExceptionPolicyStrict; import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.NeverUpToDateStep; +import com.diffplug.spotless.SerializedFunction; import com.diffplug.spotless.maven.MavenIntegrationHarness; class PluginFingerprintTest extends MavenIntegrationHarness { @@ -43,126 +46,22 @@ class PluginFingerprintTest extends MavenIntegrationHarness { private static final String VERSION_1 = "1.0.0"; private static final String VERSION_2 = "2.0.0"; - private static final String[] EXECUTION_1 = { - "", - " check", - " ", - " check", - " ", - "" - }; - private static final String[] EXECUTION_2 = {}; - - private static final String[] CONFIGURATION_1 = { - "", - " 1.2", - "" - }; - private static final String[] CONFIGURATION_2 = { - "", - " 1.8", - " true", - "" - }; - - private static final String[] DEPENDENCIES_1 = { - "", - " ", - " unknown", - " unknown", - " 1.0", - " ", - "" - }; - private static final String[] DEPENDENCIES_2 = { - "", - " ", - " unknown", - " unknown", - " 2.0", - " ", - "" - }; - private static final List FORMATTERS = singletonList(formatter(formatterStep("default"))); @Test - void sameFingerprint() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); + void sameFingerprintWhenVersionAndFormattersAreTheSame() throws Exception { + MavenProject project = mavenProject(VERSION_1); - PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); - PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); - - assertThat(fingerprint1).isEqualTo(fingerprint2); - } - - @Test - void sameFingerprintWithDependencies() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1, DEPENDENCIES_1); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1, DEPENDENCIES_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); - - PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); - PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); + PluginFingerprint fingerprint1 = PluginFingerprint.from(project, FORMATTERS); + PluginFingerprint fingerprint2 = PluginFingerprint.from(project, FORMATTERS); assertThat(fingerprint1).isEqualTo(fingerprint2); } @Test - void differentFingerprintForDifferentDependencies() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1, DEPENDENCIES_1); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1, DEPENDENCIES_2); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); - - PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); - PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); - - assertThat(fingerprint1).isNotEqualTo(fingerprint2); - } - - @Test - void differentFingerprintForDifferentPluginVersion() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - String xml2 = createPomXmlContent(VERSION_2, EXECUTION_1, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); - - PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); - PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); - - assertThat(fingerprint1).isNotEqualTo(fingerprint2); - } - - @Test - void differentFingerprintForDifferentExecution() throws Exception { - String xml1 = createPomXmlContent(VERSION_2, EXECUTION_1, CONFIGURATION_1); - String xml2 = createPomXmlContent(VERSION_2, EXECUTION_2, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); - - PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); - PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); - - assertThat(fingerprint1).isNotEqualTo(fingerprint2); - } - - @Test - void differentFingerprintForDifferentConfiguration() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_2, CONFIGURATION_2); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_2, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); + void differentFingerprintForDifferentPluginVersions() throws Exception { + MavenProject project1 = mavenProject(VERSION_1); + MavenProject project2 = mavenProject(VERSION_2); PluginFingerprint fingerprint1 = PluginFingerprint.from(project1, FORMATTERS); PluginFingerprint fingerprint2 = PluginFingerprint.from(project2, FORMATTERS); @@ -172,11 +71,8 @@ void differentFingerprintForDifferentConfiguration() throws Exception { @Test void differentFingerprintForFormattersWithDifferentSteps() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); + MavenProject project1 = mavenProject(VERSION_1); + MavenProject project2 = mavenProject(VERSION_1); FormatterStep step1 = formatterStep("step1"); FormatterStep step2 = formatterStep("step2"); @@ -192,11 +88,8 @@ void differentFingerprintForFormattersWithDifferentSteps() throws Exception { @Test void differentFingerprintForFormattersWithDifferentLineEndings() throws Exception { - String xml1 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - String xml2 = createPomXmlContent(VERSION_1, EXECUTION_1, CONFIGURATION_1); - - MavenProject project1 = mavenProject(xml1); - MavenProject project2 = mavenProject(xml2); + MavenProject project1 = mavenProject(VERSION_1); + MavenProject project2 = mavenProject(VERSION_1); FormatterStep step = formatterStep("step"); List formatters1 = singletonList(formatter(LineEnding.UNIX, step)); @@ -216,7 +109,7 @@ void emptyFingerprint() { } @Test - void failsWhenProjectDoesNotContainSpotlessPlugin() { + void failsForProjectWithoutSpotlessPlugin() { MavenProject projectWithoutSpotless = new MavenProject(); assertThatThrownBy(() -> PluginFingerprint.from(projectWithoutSpotless, FORMATTERS)) @@ -224,7 +117,39 @@ void failsWhenProjectDoesNotContainSpotlessPlugin() { .hasMessageContaining("Spotless plugin absent from the project"); } - private static MavenProject mavenProject(String xml) throws Exception { + @Test + void buildsFingerprintForProjectWithSpotlessPluginInBuildPlugins() { + MavenProject project = new MavenProject(); + Plugin spotlessPlugin = new Plugin(); + spotlessPlugin.setGroupId("com.diffplug.spotless"); + spotlessPlugin.setArtifactId("spotless-maven-plugin"); + spotlessPlugin.setVersion("1.2.3"); + project.getBuild().addPlugin(spotlessPlugin); + + PluginFingerprint fingerprint = PluginFingerprint.from(project, Collections.emptyList()); + + assertThat(fingerprint).isNotNull(); + } + + @Test + void buildsFingerprintForProjectWithSpotlessPluginInPluginManagement() { + MavenProject project = new MavenProject(); + Plugin spotlessPlugin = new Plugin(); + spotlessPlugin.setGroupId("com.diffplug.spotless"); + spotlessPlugin.setArtifactId("spotless-maven-plugin"); + spotlessPlugin.setVersion("1.2.3"); + project.getBuild().addPlugin(spotlessPlugin); + PluginManagement pluginManagement = new PluginManagement(); + pluginManagement.addPlugin(spotlessPlugin); + project.getBuild().setPluginManagement(pluginManagement); + + PluginFingerprint fingerprint = PluginFingerprint.from(project, Collections.emptyList()); + + assertThat(fingerprint).isNotNull(); + } + + private MavenProject mavenProject(String spotlessVersion) throws Exception { + String xml = createPomXmlContent(spotlessVersion, new String[0], new String[0]); return new MavenProject(readPom(xml)); } @@ -237,7 +162,7 @@ private static Model readPom(String xml) throws Exception { } private static FormatterStep formatterStep(String name) { - return FormatterStep.createNeverUpToDate(name, input -> input); + return NeverUpToDateStep.create(name, SerializedFunction.identity()); } private static Formatter formatter(FormatterStep... steps) { @@ -246,11 +171,9 @@ private static Formatter formatter(FormatterStep... steps) { private static Formatter formatter(LineEnding lineEnding, FormatterStep... steps) { return Formatter.builder() - .rootDir(Paths.get("")) .lineEndingsPolicy(lineEnding.createPolicy()) .encoding(UTF_8) .steps(Arrays.asList(steps)) - .exceptionPolicy(new FormatExceptionPolicyStrict()) .build(); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java index f315c7eefe..cfc17594ca 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,10 @@ class UpToDateCheckingTest extends MavenIntegrationHarness { + private static final String DISABLED_MESSAGE = "Up-to-date checking disabled"; + @Test - void upToDateCheckingDisabledByDefault() throws Exception { + void upToDateCheckingEnabledByDefault() throws Exception { writePom( "", " ", @@ -41,74 +43,53 @@ void upToDateCheckingDisabledByDefault() throws Exception { List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).doesNotContain("Up-to-date checking enabled"); + assertThat(output).doesNotContain(DISABLED_MESSAGE); assertFormatted(files); } @Test - void enableUpToDateChecking() throws Exception { + void explicitlyEnableUpToDateChecking() throws Exception { writePomWithUpToDateCheckingEnabled(true); List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).contains("Up-to-date checking enabled"); + assertThat(output).doesNotContain(DISABLED_MESSAGE); assertFormatted(files); } @Test - void enableUpToDateCheckingWithPluginDependencies() throws Exception { - writePomWithPluginManagementAndDependency(); + void explicitlyDisableUpToDateChecking() throws Exception { + writePomWithUpToDateCheckingEnabled(false); List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).contains("Up-to-date checking enabled"); + assertThat(output).contains(DISABLED_MESSAGE); assertFormatted(files); } @Test - void enableUpToDateCheckingWithPluginDependenciesMaven3_6_3() throws Exception { + void enableUpToDateCheckingWithPluginDependencies() throws Exception { writePomWithPluginManagementAndDependency(); - setFile(".mvn/wrapper/maven-wrapper.properties").toContent("distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip\n"); - List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).contains("Up-to-date checking enabled"); + assertThat(output).doesNotContain(DISABLED_MESSAGE); assertFormatted(files); } - private void writePomWithPluginManagementAndDependency() throws IOException { - setFile("pom.xml").toContent(createPomXmlContent("/pom-test-management.xml.mustache", - null, - null, - new String[]{ - "", - " ", - "", - "", - " true", - ""}, - new String[]{ - "", - " ", - " javax.inject", - " javax.inject", - " 1", - " ", - ""})); - } - @Test - void disableUpToDateChecking() throws Exception { - writePomWithUpToDateCheckingEnabled(false); + void enableUpToDateCheckingWithPluginDependenciesMaven3_6_3() throws Exception { + writePomWithPluginManagementAndDependency(); + + setFile(".mvn/wrapper/maven-wrapper.properties").toContent("distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip\n"); List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).doesNotContain("Up-to-date checking enabled"); + assertThat(output).doesNotContain(DISABLED_MESSAGE); assertFormatted(files); } @@ -123,7 +104,7 @@ void enableUpToDateCheckingCustomIndexFile() throws Exception { List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).contains("Up-to-date checking enabled"); + assertThat(output).doesNotContain(DISABLED_MESSAGE); assertFormatted(files); assertThat(indexFile.getParent()).exists(); assertThat(indexFile).exists(); @@ -142,7 +123,7 @@ void disableUpToDateCheckingCustomIndexFile() throws Exception { List files = writeUnformattedFiles(1); String output = runSpotlessApply(); - assertThat(output).doesNotContain("Up-to-date checking enabled"); + assertThat(output).contains(DISABLED_MESSAGE); assertFormatted(files); assertThat(indexFile.getParent()).exists(); assertThat(indexFile).doesNotExist(); @@ -214,6 +195,25 @@ void spotlessCheckRecordsUnformattedFiles() throws Exception { assertSpotlessCheckSkipped(files, checkOutput3); } + private void writePomWithPluginManagementAndDependency() throws IOException { + setFile("pom.xml").toContent(createPomXmlContent("/pom-test-management.xml.mustache", + null, + null, + new String[]{ + "", + " ", + ""}, + new String[]{ + "", + " ", + " javax.inject", + " javax.inject", + " 1", + " ", + ""}, + null)); + } + private void writePomWithUpToDateCheckingEnabled(boolean enabled) throws IOException { writePom( "", @@ -254,15 +254,15 @@ private List writeFiles(String resource, String suffix, int count) throws } private String runSpotlessApply() throws Exception { - return mavenRunnerForGoal("apply").runNoError().output(); + return mavenRunnerForGoal("apply").runNoError().stdOutUtf8(); } private String runSpotlessCheck() throws Exception { - return mavenRunnerForGoal("check").runNoError().output(); + return mavenRunnerForGoal("check").runNoError().stdOutUtf8(); } private String runSpotlessCheckOnUnformattedFiles() throws Exception { - return mavenRunnerForGoal("check").runHasError().output(); + return mavenRunnerForGoal("check").runHasError().stdOutUtf8(); } private MavenRunner mavenRunnerForGoal(String goal) throws IOException { diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/CleanthatJavaRefactorerTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/CleanthatJavaRefactorerTest.java new file mode 100644 index 0000000000..5f6bc2750d --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/CleanthatJavaRefactorerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2022-2023 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.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class CleanthatJavaRefactorerTest extends MavenIntegrationHarness { + @Test + void testEnableDraft() throws Exception { + writePomWithJavaSteps( + "", + " 11", + " true", + ""); + + runTest("MultipleMutators.dirty.test", "MultipleMutators.clean.onlyOptionalIsPresent.test"); + } + + @Test + void testLiteralsFirstInComparisons() throws Exception { + writePomWithJavaSteps( + "", + " ", + " LiteralsFirstInComparisons", + " ", + ""); + + runTest("LiteralsFirstInComparisons.dirty.test", "LiteralsFirstInComparisons.clean.test"); + } + + @Test + void testMultipleMutators_defaultIsJdk7() throws Exception { + // OptionalNotEmpty will be excluded as it is not compatible with JDK7 + writePomWithJavaSteps( + "", + " ", + " LiteralsFirstInComparisons", + " OptionalNotEmpty", + " ", + ""); + + runTest("MultipleMutators.dirty.test", "MultipleMutators.clean.onlyLiteralsFirst.test"); + } + + @Test + void testMultipleMutators_Jdk11IntroducedOptionalisPresent() throws Exception { + writePomWithJavaSteps( + "", + " 11", + " ", + " LiteralsFirstInComparisons", + " OptionalNotEmpty", + " ", + ""); + + runTest("MultipleMutators.dirty.test", "MultipleMutators.clean.test"); + } + + @Test + void testExcludeOptionalNotEmpty() throws Exception { + writePomWithJavaSteps( + "", + " ", + " LiteralsFirstInComparisons", + " OptionalNotEmpty", + " ", + " ", + " OptionalNotEmpty", + " ", + ""); + + runTest("MultipleMutators.dirty.test", "MultipleMutators.clean.onlyLiteralsFirst.test"); + } + + @Test + void testIncludeOnlyLiteralsFirstInComparisons() throws Exception { + writePomWithJavaSteps( + "", + " ", + " LiteralsFirstInComparisons", + " ", + ""); + + runTest("MultipleMutators.dirty.test", "MultipleMutators.clean.onlyLiteralsFirst.test"); + } + + private void runTest(String dirtyPath, String cleanPath) throws Exception { + String path = "src/main/java/test.java"; + setFile(path).toResource("java/cleanthat/" + dirtyPath); + // .withRemoteDebug(21654) + Assertions.assertThat(mavenRunner().withArguments("spotless:apply").runNoError().stdOutUtf8()).doesNotContain("[ERROR]"); + assertFile(path).sameAsResource("java/cleanthat/" + cleanPath); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java new file mode 100644 index 0000000000..972a4b4f7f --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 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.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class FormatAnnotationsStepTest extends MavenIntegrationHarness { + + @Test + void testFormatAnnotations() throws Exception { + writePomWithJavaSteps(""); + + String path = "src/main/java/test.java"; + setFile(path).toResource("java/formatannotations/FormatAnnotationsTestInput.test"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("java/formatannotations/FormatAnnotationsTestOutput.test"); + } + + @Test + void testFormatAnnotationsAccessModifiers() throws Exception { + writePomWithJavaSteps(""); + + String path = "src/main/java/test.java"; + setFile(path).toResource("java/formatannotations/FormatAnnotationsAccessModifiersInput.test"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("java/formatannotations/FormatAnnotationsAccessModifiersOutput.test"); + } +} 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 6fbff78cc7..52e89e58db 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-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,7 @@ */ package com.diffplug.spotless.maven.java; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; import com.diffplug.spotless.maven.MavenIntegrationHarness; @@ -27,7 +24,7 @@ class GoogleJavaFormatTest extends MavenIntegrationHarness { void specificVersionDefaultStyle() throws Exception { writePomWithJavaSteps( "", - " 1.2", + " 1.17.0", ""); runTest("java/googlejavaformat/JavaCodeFormatted.test"); @@ -37,7 +34,7 @@ void specificVersionDefaultStyle() throws Exception { void specificVersionSpecificStyle() throws Exception { writePomWithJavaSteps( "", - " 1.2", + " 1.17.0", " ", ""); @@ -45,20 +42,46 @@ void specificVersionSpecificStyle() throws Exception { } @Test - @EnabledForJreRange(min = JAVA_11) void specificVersionReflowLongStrings() throws Exception { writePomWithJavaSteps( "", - " 1.8", + " 1.17.0", " true", ""); runTest("java/googlejavaformat/JavaCodeFormattedReflowLongStrings.test"); } + @Test + void specificVersionReorderImports() throws Exception { + writePomWithJavaSteps( + "", + " 1.17.0", + " ", + " true", + ""); + + runTest("java/googlejavaformat/JavaWithReorderImportsEnabledFormatted.test", "java/googlejavaformat/JavaWithReorderImportsUnformatted.test"); + } + + @Test + void specificVersionSkipJavadocFormatting() throws Exception { + writePomWithJavaSteps( + "", + " 1.17.0", + " false", + ""); + + runTest("java/googlejavaformat/JavaCodeFormattedSkipJavadocFormatting.test"); + } + private void runTest(String targetResource) throws Exception { + runTest(targetResource, "java/googlejavaformat/JavaCodeUnformatted.test"); + } + + private void runTest(String targetResource, String sourceResource) throws Exception { String path = "src/main/java/test.java"; - setFile(path).toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + setFile(path).toResource(sourceResource); mavenRunner().withArguments("spotless:apply").runNoError(); assertFile(path).sameAsResource(targetResource); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/JavaDefaultIncludesTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/JavaDefaultIncludesTest.java new file mode 100644 index 0000000000..57f717ad07 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/JavaDefaultIncludesTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2023 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 java.io.IOException; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class JavaDefaultIncludesTest extends MavenIntegrationHarness { + + private static final String UNFORMATTED = "java/removeunusedimports/JavaCodeWithPackageUnformatted.test"; + private static final String FORMATTED = "java/removeunusedimports/JavaCodeWithPackageFormatted.test"; + + private static final String FILE_1 = "src/main/java/com/diffplug/spotless/One.java"; + private static final String FILE_2 = "src/test/java/com/diffplug/spotless/Two.java"; + private static final String FILE_3 = "src/com/diffplug/spotless/Three.java"; + private static final String FILE_4 = "test/com/diffplug/spotless/Four.java"; + private static final String FILE_5 = "foo/bar/Five.java"; + + @BeforeEach + void beforeEach() { + for (String file : Arrays.asList(FILE_1, FILE_2, FILE_3, FILE_4, FILE_5)) { + setFile(file).toResource(UNFORMATTED); + } + } + + @Test + void noCustomConfiguration() throws Exception { + writePomWithBuildConfiguration(); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Files 1, 2 are formatted because they live under default Maven source & test source dirs + assertFile(FILE_1).sameAsResource(FORMATTED); + assertFile(FILE_2).sameAsResource(FORMATTED); + + // Files 3, 4, 5 are not formatted because they live outside default Maven source & test source dirs + assertFile(FILE_3).sameAsResource(UNFORMATTED); + assertFile(FILE_4).sameAsResource(UNFORMATTED); + assertFile(FILE_5).sameAsResource(UNFORMATTED); + } + + @Test + void customSourceDirConfiguration() throws Exception { + writePomWithBuildConfiguration("src"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Files 1, 2, 3 are formatted because they live under the custom-configured source dir + assertFile(FILE_1).sameAsResource(FORMATTED); + assertFile(FILE_2).sameAsResource(FORMATTED); + assertFile(FILE_3).sameAsResource(FORMATTED); + + // File 4, 5 are not formatted because they live outside the custom-configured source dir and default test source dir + assertFile(FILE_4).sameAsResource(UNFORMATTED); + assertFile(FILE_5).sameAsResource(UNFORMATTED); + } + + @Test + void customTestSourceDirConfiguration() throws Exception { + writePomWithBuildConfiguration("test"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // File 1 is formatted because it lives under the default source dir + assertFile(FILE_1).sameAsResource(FORMATTED); + // File 4 is formatted because it lives under the custom-configured test source dir + assertFile(FILE_4).sameAsResource(FORMATTED); + + // Files 2, 3, 5 are not formatted because they live outside the default source dir and custom-configured test source dir + assertFile(FILE_2).sameAsResource(UNFORMATTED); + assertFile(FILE_3).sameAsResource(UNFORMATTED); + assertFile(FILE_5).sameAsResource(UNFORMATTED); + } + + @Test + void customSourceDirAndTestSourceDirConfiguration() throws Exception { + writePomWithBuildConfiguration( + "src", + "test"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Files 1, 2, 3, 4 are formatted because they live under custom-configured source and test source dirs + assertFile(FILE_1).sameAsResource(FORMATTED); + assertFile(FILE_2).sameAsResource(FORMATTED); + assertFile(FILE_3).sameAsResource(FORMATTED); + assertFile(FILE_4).sameAsResource(FORMATTED); + + // File 5 is not formatted because it lives outside custom-configured source and test source dirs + assertFile(FILE_5).sameAsResource(UNFORMATTED); + } + + @Test + void sameCustomSourceDirAndTestSourceDirConfiguration() throws Exception { + writePomWithBuildConfiguration( + "foo/bar", + "foo/bar"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Files 1, 2, 3, 4 are not formatted because they live outside custom-configured source and test source dirs + assertFile(FILE_1).sameAsResource(UNFORMATTED); + assertFile(FILE_2).sameAsResource(UNFORMATTED); + assertFile(FILE_3).sameAsResource(UNFORMATTED); + assertFile(FILE_4).sameAsResource(UNFORMATTED); + + // File 5 is formatted because it lives under the custom-configured source/test source dir + assertFile(FILE_5).sameAsResource(FORMATTED); + } + + @Test + void nestedCustomSourceDirAndTestSourceDirConfiguration() throws Exception { + writePomWithBuildConfiguration( + "foo", + "foo/bar"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + + // Files 1, 2, 3, 4 are not formatted because they live outside custom-configured source and test source dirs + assertFile(FILE_1).sameAsResource(UNFORMATTED); + assertFile(FILE_2).sameAsResource(UNFORMATTED); + assertFile(FILE_3).sameAsResource(UNFORMATTED); + assertFile(FILE_4).sameAsResource(UNFORMATTED); + + // File 5 is formatted because it lives under the custom-configured source/test source dir + assertFile(FILE_5).sameAsResource(FORMATTED); + } + + private void writePomWithBuildConfiguration(String... build) throws IOException { + String xml = createPomXmlContent(build, new String[]{"", "", ""}); + setFile("pom.xml").toContent(xml); + } +} 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 33ec8ea84f..eee4fab8c5 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 DiffPlug + * Copyright 2022-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,7 @@ */ package com.diffplug.spotless.maven.java; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; import com.diffplug.spotless.maven.MavenIntegrationHarness; @@ -30,23 +27,33 @@ void specificVersionDefaultStyle() throws Exception { " 1.1.0", ""); - runTest("java/palantirjavaformat/JavaCodeFormatted.test"); + runTest("java/palantirjavaformat/JavaCodeFormatted.test", "java/palantirjavaformat/JavaCodeUnformatted.test"); } @Test - @EnabledForJreRange(min = JAVA_11) void specificJava11Version2() throws Exception { writePomWithJavaSteps( "", - " 2.10.0", + " 2.39.0", + ""); + + runTest("java/palantirjavaformat/JavaCodeFormatted.test", "java/palantirjavaformat/JavaCodeUnformatted.test"); + } + + @Test + void formatJavaDoc() throws Exception { + writePomWithJavaSteps( + "", + " 2.39.0", + " true", ""); - runTest("java/palantirjavaformat/JavaCodeFormatted.test"); + runTest("java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test", "java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test"); } - private void runTest(String targetResource) throws Exception { + private void runTest(String targetResource, String sourceResource) throws Exception { String path = "src/main/java/test.java"; - setFile(path).toResource("java/palantirjavaformat/JavaCodeUnformatted.test"); + setFile(path).toResource(sourceResource); mavenRunner().withArguments("spotless:apply").runNoError(); assertFile(path).sameAsResource(targetResource); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/javascript/JavascriptFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/javascript/JavascriptFormatStepTest.java new file mode 100644 index 0000000000..d738e17dac --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/javascript/JavascriptFormatStepTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2016-2023 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.javascript; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.diffplug.spotless.ProcessRunner; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintStyleGuide; +import com.diffplug.spotless.tag.NpmTest; + +@NpmTest +class JavascriptFormatStepTest extends MavenIntegrationHarness { + + private static final String TEST_FILE_PATH = "src/main/javascript/test.js"; + + private static String styleGuideDevDependenciesString(String styleGuideName) { + return EslintStyleGuide.fromNameOrNull(styleGuideName).asMavenXmlStringMergedWith(EslintFormatterStep.defaultDevDependencies()); + } + + @NpmTest + @Nested + class EslintCustomRulesTest extends MavenIntegrationHarness { + + @Test + void eslintConfigFile() throws Exception { + writePomWithJavascriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + ""); + setFile(".eslintrc.js").toResource("npm/eslint/javascript/custom_rules/.eslintrc.js"); + setFile(TEST_FILE_PATH).toResource("npm/eslint/javascript/custom_rules/javascript-es6.dirty"); + + ProcessRunner.Result result = mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/javascript/custom_rules/javascript-es6.clean"); + } + + @Test + void eslintConfigJs() throws Exception { + final String configJs = ResourceHarness.getTestResource("npm/eslint/javascript/custom_rules/.eslintrc.js") + .replace("module.exports = ", ""); + writePomWithJavascriptSteps( + TEST_FILE_PATH, + "", + " " + configJs + "", + ""); + setFile(TEST_FILE_PATH).toResource("npm/eslint/javascript/custom_rules/javascript-es6.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/javascript/custom_rules/javascript-es6.clean"); + } + + } + + @NpmTest + @Nested + class EslintStyleguidesTest extends MavenIntegrationHarness { + + @ParameterizedTest(name = "{index}: eslint js formatting with configFile using styleguide {0}") + @ValueSource(strings = {"airbnb", "google", "standard", "xo"}) + void eslintJsStyleguideUsingConfigFile(String styleGuide) throws Exception { + final String styleGuidePath = "npm/eslint/javascript/styleguide/" + styleGuide; + + writePomWithJavascriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + " " + styleGuideDevDependenciesString(styleGuide), + ""); + setFile(".eslintrc.js").toResource(styleGuidePath + "/.eslintrc.js"); + setFile(TEST_FILE_PATH).toResource(styleGuidePath + "/javascript-es6.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource(styleGuidePath + "/javascript-es6.clean"); + } + + @ParameterizedTest(name = "{index}: eslint js formatting with inline config using styleguide {0}") + @ValueSource(strings = {"airbnb", "google", "standard", "xo"}) + void eslintJsStyleguideUsingInlineConfig(String styleGuide) throws Exception { + final String styleGuidePath = "npm/eslint/javascript/styleguide/" + styleGuide; + + final String escapedInlineConfig = ResourceHarness.getTestResource(styleGuidePath + "/.eslintrc.js") + .replace("<", "<") + .replace(">", ">"); + writePomWithJavascriptSteps( + TEST_FILE_PATH, + "", + " " + escapedInlineConfig + "", + " " + styleGuideDevDependenciesString(styleGuide), + ""); + setFile(TEST_FILE_PATH).toResource(styleGuidePath + "/javascript-es6.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource(styleGuidePath + "/javascript-es6.clean"); + } + + @Test + void provideCustomDependenciesForStyleguideStandard() throws Exception { + final String styleGuidePath = "npm/eslint/javascript/styleguide/standard"; + + writePomWithJavascriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + " ", + " 8.28.0", + " 17.0.0", + " 2.26.0", + " 15.6.0", + " 6.1.1", + " ", + ""); + setFile(".eslintrc.js").toResource(styleGuidePath + "/.eslintrc.js"); + + setFile(TEST_FILE_PATH).toResource(styleGuidePath + "/javascript-es6.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource(styleGuidePath + "/javascript-es6.clean"); + } + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java new file mode 100644 index 0000000000..4c2f05532c --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023 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.json; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class JsonTest extends MavenIntegrationHarness { + private static final Logger LOGGER = LoggerFactory.getLogger(JsonTest.class); + + @Test + public void testFormatJson_WithSimple_defaultConfig_sortByKeys() throws Exception { + writePomWithJsonSteps(""); + + setFile("json_test.json").toResource("json/sortByKeysBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfterDisabled_Simple.json"); + } + + @Test + public void testFormatJson_WithSimple_defaultConfig_nestedObject() throws Exception { + writePomWithJsonSteps(""); + + setFile("json_test.json").toResource("json/nestedObjectBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/nestedObjectAfter.json"); + } + + @Test + public void testFormatJson_WithGson_defaultConfig_sortByKeys() throws Exception { + writePomWithJsonSteps(""); + + setFile("json_test.json").toResource("json/sortByKeysBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfterDisabled.json"); + } + + @Test + public void testFormatJson_WithGson_sortByKeys() throws Exception { + writePomWithJsonSteps("true"); + + setFile("json_test.json").toResource("json/sortByKeysBefore.json"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfter.json"); + } + + @Test + public void testFormatJson_WithGson_defaultConfig_nestedObject() throws Exception { + writePomWithJsonSteps(""); + + setFile("json_test.json").toResource("json/nestedObjectBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/nestedObjectAfter.json"); + } + + @Test + public void testFormatJson_WithJackson_sortByKeys() throws Exception { + writePomWithJsonSteps("true"); + + setFile("json_test.json").toResource("json/sortByKeysBefore.json"); + + mavenRunner().withArguments("spotless:apply", "-X").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfter_Jackson.json"); + } + + @Test + public void testFormatJson_WithJackson_sortByKeys_spaceAfterKeySeparator() throws Exception { + writePomWithJsonSteps("truetrue"); + + setFile("json_test.json").toResource("json/sortByKeysBefore.json"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json"); + } + + @Test + public void testFormatJson_JsonPatch_replaceString() throws Exception { + writePomWithJsonSteps("[{\"op\":\"replace\",\"path\":\"/abc\",\"value\":\"ghi\"}]"); + + setFile("json_test.json").toResource("json/patchObjectBefore.json"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/patchObjectAfterReplaceString.json"); + } + + @Test + public void testFormatJson_JsonPatch_replaceWithObject() throws Exception { + writePomWithJsonSteps("[{\"op\":\"replace\",\"path\":\"/abc\",\"value\":{\"def\":\"ghi\"}}]"); + + setFile("json_test.json").toResource("json/patchObjectBefore.json"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("json_test.json").sameAsResource("json/patchObjectAfterReplaceWithObject.json"); + } +} 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 bd37c5c82e..dc6e59fef0 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 @@ -40,7 +40,7 @@ void testDiktatWithVersion() throws Exception { writePomWithKotlinSteps( "", - " 1.0.1", + " 1.2.1", ""); 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.0.1", + " 1.2.1", " " + conf.getAbsolutePath() + "", ""); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtfmtTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtfmtTest.java index 7b0ca069fc..68d99c8186 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtfmtTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtfmtTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,10 @@ */ package com.diffplug.spotless.maven.kotlin; -import static org.junit.jupiter.api.condition.JRE.JAVA_11; - import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; import com.diffplug.spotless.maven.MavenIntegrationHarness; -@EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. class KtfmtTest extends MavenIntegrationHarness { @Test void testKtfmt() throws Exception { @@ -40,12 +36,48 @@ void testKtfmt() throws Exception { assertFile(path2).sameAsResource("kotlin/ktfmt/basic.clean"); } + @Test + void testContinuation() throws Exception { + writePomWithKotlinSteps(""); + + setFile("src/main/kotlin/main.kt").toResource("kotlin/ktfmt/continuation.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/kotlin/main.kt").sameAsResource("kotlin/ktfmt/continuation.clean"); + } + @Test void testKtfmtStyle() throws Exception { - writePomWithKotlinSteps(""); + writePomWithKotlinSteps("0.50"); setFile("src/main/kotlin/main.kt").toResource("kotlin/ktfmt/basic.dirty"); mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("src/main/kotlin/main.kt").sameAsResource("kotlin/ktfmt/basic-dropboxstyle.clean"); } + + @Test + void testKtfmtWithMaxWidthOption() throws Exception { + writePomWithKotlinSteps("120"); + + setFile("src/main/kotlin/main.kt").toResource("kotlin/ktfmt/max-width.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/kotlin/main.kt").sameAsResource("kotlin/ktfmt/max-width.clean"); + } + + @Test + void testKtfmtStyleWithMaxWidthOption() throws Exception { + writePomWithKotlinSteps("0.17120"); + + setFile("src/main/kotlin/main.kt").toResource("kotlin/ktfmt/max-width.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/kotlin/main.kt").sameAsResource("kotlin/ktfmt/max-width-dropbox.clean"); + } + + @Test + void testKtfmtWithManageTrailingCommasOption() throws Exception { + writePomWithKotlinSteps("0.49true"); + + setFile("src/main/kotlin/main.kt").toResource("kotlin/ktfmt/trailing-commas.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/kotlin/main.kt").sameAsResource("kotlin/ktfmt/trailing-commas.clean"); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtlintTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtlintTest.java index 4e75008829..0c67143146 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtlintTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/KtlintTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,10 @@ */ package com.diffplug.spotless.maven.kotlin; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ProcessRunner; import com.diffplug.spotless.maven.MavenIntegrationHarness; class KtlintTest extends MavenIntegrationHarness { @@ -24,31 +26,75 @@ class KtlintTest extends MavenIntegrationHarness { void testKtlint() throws Exception { writePomWithKotlinSteps(""); - String path1 = "src/main/kotlin/main1.kt"; - String path2 = "src/main/kotlin/main2.kt"; + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/ktlint/basic.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("kotlin/ktlint/basic.clean"); + } - setFile(path1).toResource("kotlin/ktlint/basic.dirty"); - setFile(path2).toResource("kotlin/ktlint/basic.dirty"); + @Test + void testKtlintEditorConfigOverride() throws Exception { + writePomWithKotlinSteps("\n" + + " \n" + + " true\n" + + " true\n" + + " \n" + + ""); + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.clean"); + } - assertFile(path1).sameAsResource("kotlin/ktlint/basic.clean"); - assertFile(path2).sameAsResource("kotlin/ktlint/basic.clean"); + @Test + void testReadCodeStyleFromEditorConfigFile() throws Exception { + setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); + writePomWithKotlinSteps(""); + checkKtlintOfficialStyle(); } @Test - void testKtlintShyiko() throws Exception { - writePomWithKotlinSteps("0.21.0"); + void testEditorConfigOverrideWithUnsetCodeStyleDoesNotOverrideEditorConfigCodeStyleWithDefault() throws Exception { + setFile(".editorconfig").toResource("kotlin/ktlint/ktlint_official/.editorconfig"); + writePomWithKotlinSteps("\n" + + " \n" + + " true\n" + + " \n" + + ""); + checkKtlintOfficialStyle(); + } - String path1 = "src/main/kotlin/main1.kt"; - String path2 = "src/main/kotlin/main2.kt"; + @Test + void testSetEditorConfigCanOverrideEditorConfigFile() throws Exception { + setFile(".editorconfig").toResource("kotlin/ktlint/intellij_idea/.editorconfig"); + writePomWithKotlinSteps("\n" + + " \n" + + " ktlint_official\n" + + " \n" + + ""); + checkKtlintOfficialStyle(); + } - setFile(path1).toResource("kotlin/ktlint/basic.dirty"); - setFile(path2).toResource("kotlin/ktlint/basic.dirty"); + @Test + void testWithCustomRuleSetApply() throws Exception { + writePomWithKotlinSteps("\n" + + " \n" + + " io.nlopez.compose.rules:ktlint:0.4.16\n" + + " \n" + + " \n" + + " Composable\n" + + " \n" + + ""); + setFile("src/main/kotlin/Main.kt").toResource("kotlin/ktlint/listScreen.dirty"); + ProcessRunner.Result result = mavenRunner().withArguments("spotless:check").runHasError(); + Assertions.assertThat(result.toString()).contains("Composable functions that return Unit should start with an uppercase letter."); + } + private void checkKtlintOfficialStyle() throws Exception { + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); mavenRunner().withArguments("spotless:apply").runNoError(); - - assertFile(path1).sameAsResource("kotlin/ktlint/basic.clean"); - assertFile(path2).sameAsResource("kotlin/ktlint/basic.clean"); + assertFile(path).sameAsResource("kotlin/ktlint/experimentalEditorConfigOverride.ktlintOfficial.clean"); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java index a623ca7f3c..eb1c6cc95f 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/markdown/FlexmarkMavenTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,7 @@ public void testFlexmarkWithDefaultConfig() throws Exception { writePomWithMarkdownSteps(""); setFile("markdown_test.md").toResource("markdown/flexmark/FlexmarkUnformatted.md"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("markdown_test.md").sameAsResource("markdown/flexmark/FlexmarkFormatted.md"); } - } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java new file mode 100644 index 0000000000..de030ab81c --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.npm; + +/** + * Helper class to configure a maven pom to use frontend-maven-plugin. + * @see frontend-maven-plugin on github + */ +public final class NpmFrontendMavenPlugin { + + public static final String GROUP_ID = "com.github.eirslett"; + public static final String ARTIFACT_ID = "frontend-maven-plugin"; + public static final String VERSION = "1.11.3"; // for now, we stick with this version, as it is the last to support maven 3.1 (see pom-test.xml.mustache) + + public static final String GOAL_INSTALL_NODE_AND_NPM = "install-node-and-npm"; + + public static final String INSTALL_DIRECTORY = "target"; + + private NpmFrontendMavenPlugin() { + // prevent instantiation + } + + public static String[] pomPluginLines(String nodeVersion, String npmVersion) { + return new String[]{ + "", + String.format(" %s", GROUP_ID), + String.format(" %s", ARTIFACT_ID), + String.format(" %s", VERSION), + " ", + " ", + " install node and npm", + " ", + String.format(" %s", GOAL_INSTALL_NODE_AND_NPM), + " ", + " ", + " ", + " ", + (nodeVersion != null ? " " + nodeVersion + "" : ""), + (npmVersion != null ? " " + npmVersion + "" : ""), + String.format(" %s", INSTALL_DIRECTORY), + " ", + "" + }; + } + + public static String installNpmMavenGoal() { + return String.format("%s:%s:%s", GROUP_ID, ARTIFACT_ID, GOAL_INSTALL_NODE_AND_NPM); + } + + public static String installedNpmPath() { + return String.format("%s/node/npm%s", INSTALL_DIRECTORY, System.getProperty("os.name").toLowerCase().contains("win") ? ".cmd" : ""); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmStepsWithNpmInstallCacheTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmStepsWithNpmInstallCacheTest.java new file mode 100644 index 0000000000..eabdd5c5fc --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmStepsWithNpmInstallCacheTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2023-2024 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.npm; + +import static com.diffplug.spotless.maven.npm.AbstractNpmFormatterStepFactory.SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.ProcessRunner.Result; +import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.tag.NpmTest; + +@NpmTest +public class NpmStepsWithNpmInstallCacheTest extends MavenIntegrationHarness { + + // TODO implement tests without cache and with various cache paths + // using only prettier is enough since the other cases are covered by gradle-side integration tests + + @Test + void prettierTypescriptWithoutCache() throws Exception { + String suffix = "ts"; + writePomWithPrettierSteps("**/*." + suffix, + "", + " 1.16.4", + " .prettierrc.yml", + ""); + Result result = run("typescript", suffix); + Assertions.assertThat(result.stdOutUtf8()).doesNotContain("Caching node_modules for").doesNotContain("Using cached node_modules for"); + } + + @Test + void prettierTypescriptWithDefaultCache() throws Exception { + String suffix = "ts"; + writePomWithPrettierSteps("**/*." + suffix, + "", + " 1.16.4", + " .prettierrc.yml", + " true", + ""); + Result result = run("typescript", suffix); + Assertions.assertThat(result.stdOutUtf8()) + .contains("Caching node_modules for") + .contains(SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME) + .doesNotContain("Using cached node_modules for"); + } + + @Disabled + @Test + void prettierTypescriptWithDefaultCacheIsReusedOnSecondRun() throws Exception { + String suffix = "ts"; + writePomWithPrettierSteps("**/*." + suffix, + "", + " 1.16.4", + " .prettierrc.yml", + " true", + ""); + Result result1 = run("typescript", suffix); + Assertions.assertThat(result1.stdOutUtf8()) + .contains("Caching node_modules for") + .contains(SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME) + .doesNotContain("Using cached node_modules for"); + + // recursively delete target folder to simulate a fresh run (except the default cache folder) + recursiveDelete(Paths.get(rootFolder().getAbsolutePath(), "target"), SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME); + + Result result2 = run("typescript", suffix); + Assertions.assertThat(result2.stdOutUtf8()) + .doesNotContain("Caching node_modules for") + .contains(SPOTLESS_NPM_INSTALL_CACHE_DEFAULT_NAME) + .contains("Using cached node_modules for"); + } + + @Test + void prettierTypescriptWithSpecificCache() throws Exception { + String suffix = "ts"; + File cacheDir = newFolder("cache-prettier-1"); + writePomWithPrettierSteps("**/*." + suffix, + "", + " 1.16.4", + " .prettierrc.yml", + " " + cacheDir.getAbsolutePath() + "", + ""); + Result result = run("typescript", suffix); + Assertions.assertThat(result.stdOutUtf8()) + .contains("Caching node_modules for") + .contains(Path.of(cacheDir.getAbsolutePath()).toAbsolutePath().toString()) + .doesNotContain("Using cached node_modules for"); + } + + @Disabled + @Test + void prettierTypescriptWithSpecificCacheIsUsedOnSecondRun() throws Exception { + String suffix = "ts"; + File cacheDir = newFolder("cache-prettier-1"); + writePomWithPrettierSteps("**/*." + suffix, + "", + " 1.16.4", + " .prettierrc.yml", + " " + cacheDir.getAbsolutePath() + "", + ""); + Result result1 = run("typescript", suffix); + Assertions.assertThat(result1.stdOutUtf8()) + .contains("Caching node_modules for") + .contains(Path.of(cacheDir.getAbsolutePath()).toAbsolutePath().toString()) + .doesNotContain("Using cached node_modules for"); + + // recursively delete target folder to simulate a fresh run + recursiveDelete(Paths.get(rootFolder().getAbsolutePath(), "target"), null); + + Result result2 = run("typescript", suffix); + Assertions.assertThat(result2.stdOutUtf8()) + .doesNotContain("Caching node_modules for") + .contains(Path.of(cacheDir.getAbsolutePath()).toAbsolutePath().toString()) + .contains("Using cached node_modules for"); + } + + private void recursiveDelete(Path path, String exclusion) throws IOException { + Files.walkFileTree(path, new RecursiveDelete(exclusion)); + } + + private Result run(String kind, String suffix) throws IOException, InterruptedException { + String path = prepareRun(kind, suffix); + Result result = mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("npm/prettier/filetypes/" + kind + "/" + kind + ".clean"); + return result; + } + + private String prepareRun(String kind, String suffix) throws IOException { + String configPath = ".prettierrc.yml"; + setFile(configPath).toResource("npm/prettier/filetypes/" + kind + "/" + ".prettierrc.yml"); + String path = "src/main/" + kind + "/test." + suffix; + setFile(path).toResource("npm/prettier/filetypes/" + kind + "/" + kind + ".dirty"); + return path; + } + + private static class RecursiveDelete extends SimpleFileVisitor { + private final String exclusionDirectory; + + public RecursiveDelete(String exclusionDirectory) { + this.exclusionDirectory = exclusionDirectory; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (exclusionDirectory != null && dir.toFile().getName().equals(exclusionDirectory)) { + return FileVisitResult.SKIP_SUBTREE; + } + return super.preVisitDirectory(dir, attrs); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return super.visitFile(file, attrs); + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (dir.toFile().listFiles().length != 0) { + // skip non-empty dir + return super.postVisitDirectory(dir, exc); + } + Files.delete(dir); + return super.postVisitDirectory(dir, exc); + } + } +} 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 new file mode 100644 index 0000000000..78830bf09d --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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.npm; + +import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.installNpmMavenGoal; +import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.installedNpmPath; +import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.pomPluginLines; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class NpmTestsWithDynamicallyInstalledNpmInstallationTest extends MavenIntegrationHarness { + + @Test + void useDownloadedNpmInstallation() throws Exception { + writePomWithPrettierSteps( + pomPluginLines("v18.13.0", null), + "src/main/typescript/test.ts", + "", + " " + installedNpmPath() + "", + ""); + + String kind = "typescript"; + String suffix = "ts"; + String configPath = ".prettierrc.yml"; + setFile(configPath).toResource("npm/prettier/filetypes/" + kind + "/" + ".prettierrc.yml"); + String path = "src/main/" + kind + "/test." + suffix; + setFile(path).toResource("npm/prettier/filetypes/" + kind + "/" + kind + ".dirty"); + + mavenRunner().withArguments(installNpmMavenGoal(), "spotless:apply").runNoError(); + assertFile(path).sameAsResource("npm/prettier/filetypes/" + kind + "/" + kind + ".clean"); + } + +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/pom/SortPomMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/pom/SortPomMavenTest.java index ca9d77a6e3..93eb8a9633 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/pom/SortPomMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/pom/SortPomMavenTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ public void testSortPomWithDefaultConfig() throws Exception { writePomWithPomSteps(""); setFile("pom_test.xml").toResource("pom/pom_dirty.xml"); - mavenRunner().withArguments("spotless:apply").runNoError().error(); + mavenRunner().withArguments("spotless:apply").runNoError(); assertFile("pom_test.xml").sameAsResource("pom/pom_clean_default.xml"); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java index 130bbde600..abba35e72c 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ProcessRunner; import com.diffplug.spotless.maven.MavenIntegrationHarness; -import com.diffplug.spotless.maven.MavenRunner.Result; import com.diffplug.spotless.maven.generic.Prettier; import com.diffplug.spotless.tag.NpmTest; @@ -43,7 +43,7 @@ private String prepareRun(String kind, String suffix) throws IOException { return path; } - private Result runExpectingError(String kind, String suffix) throws IOException, InterruptedException { + private ProcessRunner.Result runExpectingError(String kind, String suffix) throws IOException, InterruptedException { String path = prepareRun(kind, suffix); return mavenRunner().withArguments("spotless:apply").runHasError(); } @@ -102,8 +102,60 @@ void unique_dependency_config() throws Exception { " 1.16.4", ""); - Result result = mavenRunner().withArguments("spotless:apply").runHasError(); - assertThat(result.output()).contains(Prettier.ERROR_MESSAGE_ONLY_ONE_CONFIG); + ProcessRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertThat(result.stdOutUtf8()).contains(Prettier.ERROR_MESSAGE_ONLY_ONE_CONFIG); + } + + /** + * This test is to ensure that we can have multiple prettier instances in one spotless config. + * + * @see Issue #1162 on github + */ + @Test + void multiple_prettier_configs() throws Exception { + writePom( + formats( + groupWithSteps("format", including("php-example.php"), + "", + " ", + " ", + " prettier", + " 2.8.8", + " ", + " ", + " @prettier/plugin-php", + " 0.19.6", + " ", + " ", + " ", + " 3", + " php", + " ", + ""), + groupWithSteps("java", including("JavaTest.java"), + "", + " ", + " ", + " prettier", + " 2.8.8", + " ", + " ", + " prettier-plugin-java", + " 2.2.0", + " ", + " ", + " ", + " 4", + " java", + " ", + ""))); + + setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); + assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); + } @Test @@ -114,16 +166,43 @@ void custom_plugin() throws Exception { " ", " ", " prettier", - " 2.0.5", + " 2.8.8", + " ", + " ", + " @prettier/plugin-php", + " 0.19.6", + " ", + " ", + " ", + " 3", + " php", + " ", + ""); + + setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); + } + + @Test + void custom_plugin_prettier3() throws Exception { + writePomWithFormatSteps( + "php-example.php", + "", + " ", + " ", + " prettier", + " 3.0.3", " ", " ", " @prettier/plugin-php", - " 0.14.2", + " 0.20.1", " ", " ", " ", " 3", " php", + " @prettier/plugin-php", " ", ""); @@ -132,6 +211,29 @@ void custom_plugin() throws Exception { assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); } + @Test + void custom_plugin_prettier3_file_config() throws Exception { + writePomWithFormatSteps( + "JavaTest.java", + "", + " ", + " ", + " prettier", + " 3.0.3", + " ", + " ", + " prettier-plugin-java", + " 2.3.0", + " ", + " ", + " .prettierrc.yml", + ""); + setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc_java_plugin.yml"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); + } + @Test void autodetect_parser_based_on_filename() throws Exception { writePomWithFormatSteps( @@ -145,20 +247,28 @@ void autodetect_parser_based_on_filename() throws Exception { @Test void autodetect_npmrc_file() throws Exception { - setFile(".npmrc").toLines("registry=https://i.do.no.exist.com"); + setFile(".npmrc").toLines( + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); String suffix = "ts"; writePomWithPrettierSteps("**/*." + suffix, "", " 1.16.4", " .prettierrc.yml", ""); - Result result = runExpectingError("typescript", suffix); - assertThat(result.output()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); + ProcessRunner.Result result = runExpectingError("typescript", suffix); + assertThat(result.stdOutUtf8()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); } @Test void select_configured_npmrc_file() throws Exception { - setFile(".custom_npmrc").toLines("registry=https://i.do.no.exist.com"); + setFile(".custom_npmrc").toLines( + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); String suffix = "ts"; writePomWithPrettierSteps("**/*." + suffix, "", @@ -166,7 +276,7 @@ void select_configured_npmrc_file() throws Exception { " .prettierrc.yml", " ${basedir}/.custom_npmrc", ""); - Result result = runExpectingError("typescript", suffix); - assertThat(result.output()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); + ProcessRunner.Result result = runExpectingError("typescript", suffix); + assertThat(result.stdOutUtf8()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/protobuf/BufMavenIntegrationTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/protobuf/BufMavenIntegrationTest.java new file mode 100644 index 0000000000..4f07c06772 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/protobuf/BufMavenIntegrationTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.protobuf; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.tag.BufTest; + +@BufTest +class BufMavenIntegrationTest extends MavenIntegrationHarness { + @Test + void buf() throws Exception { + writePomWithProtobufSteps("", ""); + setFile("buf.proto").toResource("protobuf/buf/buf.proto"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf.proto.clean"); + } + + @Test + void bufLarge() throws Exception { + writePomWithProtobufSteps("", ""); + setFile("buf.proto").toResource("protobuf/buf/buf_large.proto"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf_large.proto.clean"); + } + + @Test + void bufWithLicense() throws Exception { + writePomWithProtobufSteps( + "", + "", + "", + " /* (C) 2022 */", + ""); + setFile("buf.proto").toResource("protobuf/buf/license.proto"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("buf.proto").sameAsResource("protobuf/buf/license.proto.clean"); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java new file mode 100644 index 0000000000..534835c644 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/shell/ShellTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.shell; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; +import com.diffplug.spotless.tag.ShfmtTest; + +@ShfmtTest +public class ShellTest extends MavenIntegrationHarness { + @Test + public void testFormatShellWithEditorconfig() throws Exception { + String fileDir = "shell/shfmt/with-config/"; + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + setFile(".editorconfig").toResource(fileDir + ".editorconfig"); + + writePomWithShellSteps(""); + mavenRunner().withArguments("spotless:apply").runNoError(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } + + @Test + public void testFormatShellWithoutEditorconfig() throws Exception { + String fileDir = "shell/shfmt/without-config/"; + setFile("shfmt.sh").toResource(fileDir + "shfmt.sh"); + setFile("scripts/other.sh").toResource(fileDir + "other.sh"); + + writePomWithShellSteps(""); + mavenRunner().withArguments("spotless:apply").runNoError(); + + assertFile("shfmt.sh").sameAsResource(fileDir + "shfmt.clean"); + assertFile("scripts/other.sh").sameAsResource(fileDir + "other.clean"); + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/typescript/TypescriptFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/typescript/TypescriptFormatStepTest.java index 600f9818d1..afda22e06b 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/typescript/TypescriptFormatStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/typescript/TypescriptFormatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,86 +19,104 @@ import java.io.IOException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ProcessRunner; +import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.maven.MavenIntegrationHarness; -import com.diffplug.spotless.maven.MavenRunner.Result; +import com.diffplug.spotless.npm.EslintFormatterStep; +import com.diffplug.spotless.npm.EslintStyleGuide; import com.diffplug.spotless.tag.NpmTest; @NpmTest class TypescriptFormatStepTest extends MavenIntegrationHarness { - private void run(String kind) throws IOException, InterruptedException { - String path = prepareRun(kind); + + private static final String TEST_FILE_PATH = "src/main/typescript/test.ts"; + + private static String styleGuideDevDependenciesString(String styleGuideName) { + return EslintStyleGuide.fromNameOrNull(styleGuideName).asMavenXmlStringMergedWith(EslintFormatterStep.defaultDevDependencies()); + } + + private void runTsfmt(String kind) throws IOException, InterruptedException { + String path = prepareRunTsfmt(kind); mavenRunner().withArguments("spotless:apply").runNoError(); assertFile(path).sameAsResource("npm/tsfmt/" + kind + "/" + kind + ".clean"); } - private String prepareRun(String kind) throws IOException { - String path = "src/main/typescript/test.ts"; - setFile(path).toResource("npm/tsfmt/" + kind + "/" + kind + ".dirty"); - return path; + private String prepareRunTsfmt(String kind) throws IOException { + setFile(TEST_FILE_PATH).toResource("npm/tsfmt/" + kind + "/" + kind + ".dirty"); + return TEST_FILE_PATH; } - private Result runExpectingError(String kind) throws IOException, InterruptedException { - prepareRun(kind); + private ProcessRunner.Result runExpectingErrorTsfmt(String kind) throws IOException, InterruptedException { + prepareRunTsfmt(kind); return mavenRunner().withArguments("spotless:apply").runHasError(); } @Test void tslint() throws Exception { writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${basedir}/tslint.json", ""); setFile("tslint.json").toResource("npm/tsfmt/tslint/tslint.json"); - run("tslint"); + runTsfmt("tslint"); } @Test void vscode() throws Exception { writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${basedir}/vscode.json", ""); setFile("vscode.json").toResource("npm/tsfmt/vscode/vscode.json"); - run("vscode"); + runTsfmt("vscode"); } @Test void tsfmt() throws Exception { writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${basedir}/tsfmt.json", ""); setFile("tsfmt.json").toResource("npm/tsfmt/tsfmt/tsfmt.json"); - run("tsfmt"); + runTsfmt("tsfmt"); } @Test void tsfmtInline() throws Exception { writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ", " 1", " true", " ", ""); - run("tsfmt"); + runTsfmt("tsfmt"); } @Test void tsconfig() throws Exception { writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${project.basedir}/tsconfig.json", ""); setFile("tsconfig.json").toResource("npm/tsfmt/tsconfig/tsconfig.json"); - run("tsconfig"); + runTsfmt("tsconfig"); } @Test void testTypescript_2_Configs() throws Exception { + String path = "src/main/typescript/test.ts"; + writePomWithTypescriptSteps( + path, "", " ${basedir}/tslint.json", " ${basedir}/tslint.json", @@ -106,34 +124,107 @@ void testTypescript_2_Configs() throws Exception { setFile("vscode.json").toResource("npm/tsfmt/vscode/vscode.json"); setFile("tsfmt.json").toResource("npm/tsfmt/tsfmt/tsfmt.json"); - String path = "src/main/typescript/test.ts"; setFile(path).toResource("npm/tsfmt/tsfmt/tsfmt.dirty"); - Result result = mavenRunner().withArguments("spotless:apply").runHasError(); - assertThat(result.output()).contains("must specify exactly one configFile or config"); + ProcessRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertThat(result.stdOutUtf8()).contains("must specify exactly one configFile or config"); } @Test void testNpmrcIsAutoPickedUp() throws Exception { - setFile(".npmrc").toLines("registry=https://i.do.no.exist.com"); + setFile(".npmrc").toLines( + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${basedir}/tslint.json", ""); setFile("tslint.json").toResource("npm/tsfmt/tslint/tslint.json"); - Result result = runExpectingError("tslint"); - assertThat(result.output()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); + ProcessRunner.Result result = runExpectingErrorTsfmt("tslint"); + assertThat(result.stdOutUtf8()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); } @Test void testNpmrcIsConfigurativelyPickedUp() throws Exception { - setFile(".custom_npmrc").toLines("registry=https://i.do.no.exist.com"); + setFile(".custom_npmrc").toLines( + "registry=https://i.do.not.exist.com", + "fetch-timeout=250", + "fetch-retry-mintimeout=250", + "fetch-retry-maxtimeout=250"); writePomWithTypescriptSteps( + TEST_FILE_PATH, "", " ${basedir}/tslint.json", " ${basedir}/.custom_npmrc", ""); setFile("tslint.json").toResource("npm/tsfmt/tslint/tslint.json"); - Result result = runExpectingError("tslint"); - assertThat(result.output()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); + ProcessRunner.Result result = runExpectingErrorTsfmt("tslint"); + assertThat(result.stdOutUtf8()).containsPattern("Running npm command.*npm install.* failed with exit code: 1"); + } + + @Test + void eslintConfigFile() throws Exception { + writePomWithTypescriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + ""); + setFile(".eslintrc.js").toResource("npm/eslint/typescript/custom_rules/.eslintrc.js"); + setFile(TEST_FILE_PATH).toResource("npm/eslint/typescript/custom_rules/typescript.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/typescript/custom_rules/typescript.clean"); + } + + @Test + void eslintConfigJs() throws Exception { + final String configJs = ResourceHarness.getTestResource("npm/eslint/typescript/custom_rules/.eslintrc.js") + .replace("module.exports = ", ""); + writePomWithTypescriptSteps( + TEST_FILE_PATH, + "", + " " + configJs + "", + ""); + setFile(TEST_FILE_PATH).toResource("npm/eslint/typescript/custom_rules/typescript.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/typescript/custom_rules/typescript.clean"); + } + + @Test + void eslintStyleguideStandardWithTypescript() throws Exception { + writePomWithTypescriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + " " + styleGuideDevDependenciesString("standard-with-typescript"), + " ${basedir}/tsconfig.json", + ""); + setFile(".eslintrc.js").toResource("npm/eslint/typescript/styleguide/standard_with_typescript/.eslintrc.js"); + setFile("tsconfig.json").toResource("npm/eslint/typescript/styleguide/standard_with_typescript/tsconfig.json"); + setFile(TEST_FILE_PATH).toResource("npm/eslint/typescript/styleguide/standard_with_typescript/typescript.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean"); + } + + @Test + @Disabled + void eslintStyleguideXo() throws Exception { + writePomWithTypescriptSteps( + TEST_FILE_PATH, + "", + " .eslintrc.js", + " " + styleGuideDevDependenciesString("xo-typescript"), + " ${basedir}/tsconfig.json", + ""); + setFile(".eslintrc.js").toResource("npm/eslint/typescript/styleguide/xo/.eslintrc.js"); + setFile("tsconfig.json").toResource("npm/eslint/typescript/styleguide/xo/tsconfig.json"); + setFile(TEST_FILE_PATH).toResource("npm/eslint/typescript/styleguide/xo/typescript.dirty"); + + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(TEST_FILE_PATH).sameAsResource("npm/eslint/typescript/styleguide/xo/typescript.clean"); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java new file mode 100644 index 0000000000..6af4d09ce9 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 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.yaml; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class YamlTest extends MavenIntegrationHarness { + private static final Logger LOGGER = LoggerFactory.getLogger(YamlTest.class); + + @Test + public void testFormatYaml_WithJackson_defaultConfig_separatorComments() throws Exception { + writePomWithYamlSteps(""); + + setFile("yaml_test.yaml").toResource("yaml/separator_comments.yaml"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("yaml_test.yaml").sameAsResource("yaml/separator_comments.clean.yaml"); + } + + @Test + public void testFormatYaml_WithJackson_defaultConfig_arrayBrackets() throws Exception { + writePomWithYamlSteps(""); + + setFile("yaml_test.yaml").toResource("yaml/array_with_bracket.yaml"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("yaml_test.yaml").sameAsResource("yaml/array_with_bracket.clean.yaml"); + } + + @Test + public void testFormatYaml_WithJackson_defaultConfig_multipleDocuments() throws Exception { + writePomWithYamlSteps(""); + + setFile("yaml_test.yaml").toResource("yaml/multiple_documents.yaml"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("yaml_test.yaml").sameAsResource("yaml/multiple_documents.clean.jackson.yaml"); + } +} diff --git a/plugin-maven/src/test/resources/pom-build.xml.mustache b/plugin-maven/src/test/resources/pom-build.xml.mustache deleted file mode 100644 index 3c1d1a7d5b..0000000000 --- a/plugin-maven/src/test/resources/pom-build.xml.mustache +++ /dev/null @@ -1,96 +0,0 @@ - - 4.0.0 - - com.diffplug.spotless - spotless-maven-plugin - {{spotlessMavenPluginVersion}} - maven-plugin - - Spotless Maven Plugin - - - - 3.1.0 - - - - UTF-8 - 1.8 - 1.8 - - - {{mavenApiVersion}} - {{eclipseAetherVersion}} - {{spotlessLibVersion}} - {{jsr305Version}} - - - - - org.apache.maven - maven-core - ${maven.api.version} - provided - - - org.apache.maven - maven-plugin-api - ${maven.api.version} - provided - - - org.apache.maven.plugin-tools - maven-plugin-annotations - ${maven.api.version} - provided - - - org.eclipse.aether - aether-api - ${eclipse.aether.version} - provided - - - org.eclipse.aether - aether-util - ${eclipse.aether.version} - provided - - - com.google.code.findbugs - jsr305 - ${jsr305.version} - provided - - - - com.diffplug.spotless - spotless-lib - ${spotless.lib.version} - - - com.diffplug.spotless - spotless-lib-extra - ${spotless.lib.version} - - -{{{additionalDependencies}}} - - - - - - - - org.apache.maven.plugins - maven-plugin-plugin - 3.5 - - - - - diff --git a/plugin-maven/src/test/resources/pom-test.xml.mustache b/plugin-maven/src/test/resources/pom-test.xml.mustache index 122f5d9218..f87e8c1a3e 100644 --- a/plugin-maven/src/test/resources/pom-test.xml.mustache +++ b/plugin-maven/src/test/resources/pom-test.xml.mustache @@ -20,7 +20,9 @@ + {{{build}}} + {{{plugins}}} com.diffplug.spotless spotless-maven-plugin diff --git a/settings.gradle b/settings.gradle index f335936482..eb1c77e4ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,32 +1,36 @@ pluginManagement { - plugins { - id 'com.diffplug.spotless' version '6.1.2' - // https://plugins.gradle.org/plugin/com.gradle.plugin-publish - id 'com.gradle.plugin-publish' version '0.19.0' - // https://github.com/gradle-nexus/publish-plugin/releases - id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' - // https://github.com/spotbugs/spotbugs-gradle-plugin/releases - id 'com.github.spotbugs' version '5.0.4' - // https://github.com/diffplug/spotless-changelog - id 'com.diffplug.spotless-changelog' version '2.3.2' - // https://github.com/diffplug/goomph/blob/main/CHANGES.md - id 'com.diffplug.p2.asmaven' version '3.27.0' // DO NOT UPDATE, see https://github.com/diffplug/spotless/pull/874 - // https://github.com/gradle/test-retry-gradle-plugin/releases - id 'org.gradle.test-retry' version '1.3.1' - // https://github.com/radarsh/gradle-test-logger-plugin/blob/develop/CHANGELOG.md - id 'com.adarshr.test-logger' version '3.1.0' + repositories { + mavenCentral() + gradlePluginPortal() } } + plugins { - id 'com.diffplug.spotless' apply false - id 'com.gradle.plugin-publish' apply false - id 'io.github.gradle-nexus.publish-plugin' apply false - id 'com.github.spotbugs' apply false - id 'com.diffplug.spotless-changelog' apply false - id 'com.diffplug.p2.asmaven' apply false - id 'org.gradle.test-retry' apply false - id 'com.adarshr.test-logger' apply false + id 'com.diffplug.spotless' version '7.0.3' apply false + // https://plugins.gradle.org/plugin/com.gradle.plugin-publish + id 'com.gradle.plugin-publish' version '1.3.1' apply false + // https://github.com/gradle-nexus/publish-plugin/releases + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' apply false + // https://github.com/spotbugs/spotbugs-gradle-plugin/releases + id 'com.github.spotbugs' version '6.1.7' apply false + // https://github.com/diffplug/spotless-changelog/blob/main/CHANGELOG.md + id 'com.diffplug.spotless-changelog' version '3.1.2' apply false + // https://github.com/radarsh/gradle-test-logger-plugin/blob/develop/CHANGELOG.md + id 'com.adarshr.test-logger' version '4.0.0' apply false + // https://github.com/davidburstrom/version-compatibility-gradle-plugin/tags + id 'io.github.davidburstrom.version-compatibility' version '0.5.0' apply false + // https://plugins.gradle.org/plugin/com.gradle.develocity + id 'com.gradle.develocity' version '3.19.2' + // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md + id 'dev.equo.ide' version '1.7.8' apply false +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } } + if (System.env['CI'] != null) { // use the remote buildcache on all CI builds buildCache { @@ -55,6 +59,21 @@ if (System.env['CI'] != null) { } } +def isCI = providers.environmentVariable('CI').present + +develocity { + buildScan { + termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseAgree = "yes" + publishing { + onlyIf { isCI } + } + } +} + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + rootProject.name = 'spotless' include 'lib' // reusable library with no dependencies @@ -75,37 +94,8 @@ def getStartProperty(java.lang.String name) { userProperties.load(userPropertiesFile.newReader()) } return userProperties.get(name) - } -if (System.getenv('SPOTLESS_EXCLUDE_MAVEN') != 'true' && getStartProperty('SPOTLESS_EXCLUDE_MAVEN') != 'true' && System.getenv('JITPACK') != 'true') { +if (System.getenv('SPOTLESS_EXCLUDE_MAVEN') != 'true' && getStartProperty('SPOTLESS_EXCLUDE_MAVEN') != 'true') { include 'plugin-maven' // maven-specific glue code } - -// include external (_ext) projects from development builds -if (getStartProperty('com.diffplug.spotless.include.ext') == 'true') { - file('_ext').eachDirMatch(~/^(?!(\.|gradle)).*/) { dir -> - include dir.name - project(":${dir.name}").projectDir = dir - } -} - -// include external (_ext) projects from development builds, but disable the p2 parts -if (getStartProperty('com.diffplug.spotless.include.ext.nop2') == 'true') { - file('_ext').eachDirMatch(~/^(?!(\.|gradle)).*/) { dir -> - include dir.name - project(":${dir.name}").projectDir = dir - } -} - -// the p2-based projects are too expensive for routine CI builds, so they have to be invoked explicitly -for (kind in [ - 'cdt', - 'groovy', - 'wtp' - ]) { - if (getStartProperty("com.diffplug.spotless.include.ext.${kind}") == 'true') { - include "eclipse-${kind}" - project(":eclipse-${kind}").projectDir = file("_ext/eclipse-${kind}") - } -} diff --git a/testlib/build.gradle b/testlib/build.gradle index 8e0a012882..2d10006a60 100644 --- a/testlib/build.gradle +++ b/testlib/build.gradle @@ -6,38 +6,41 @@ version = rootProject.spotlessChangelog.versionNext apply from: rootProject.file('gradle/java-setup.gradle') dependencies { - api project(':lib') - api files(project(':lib').sourceSets.sortPom.output.classesDirs) + api projects.lib + api files(projects.lib.dependencyProject.sourceSets.sortPom.output.classesDirs) api "com.diffplug.durian:durian-core:${VER_DURIAN}" api "com.diffplug.durian:durian-testlib:${VER_DURIAN}" api "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" api "org.assertj:assertj-core:${VER_ASSERTJ}" + api "org.mockito:mockito-core:$VER_MOCKITO" + api "com.diffplug.selfie:selfie-lib:${VER_SELFIE}" + api "com.diffplug.selfie:selfie-runner-junit5:${VER_SELFIE}" + runtimeOnly "org.junit.platform:junit-platform-launcher" implementation "com.diffplug.durian:durian-io:${VER_DURIAN}" implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" + implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}" implementation gradleTestKit() } // we'll hold the testlib to a low standard (prize brevity) -spotbugs { reportLevel = 'high' } // low|medium|high (low = sensitive to even minor mistakes) +spotbugs { + // LOW|MEDIUM|DEFAULT|HIGH (low = sensitive to even minor mistakes). + reportLevel = com.github.spotbugs.snom.Confidence.valueOf('HIGH') +} -test { - useJUnitPlatform() +apply from: rootProject.file('gradle/special-tests.gradle') +tasks.withType(Test).configureEach { if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) { - // for GJF https://github.com/diffplug/spotless/issues/834 + // for Antlr4FormatterStepTest, KtfmtStepTest, and KtLintStepTest def args = [ - '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', - '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', - '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', - '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', ] jvmArgs args } } -apply from: rootProject.file('gradle/special-tests.gradle') - javadoc { options.addStringOption('Xdoclint:none', '-quiet') options.addStringOption('Xwerror', '-quiet') diff --git a/testlib/src/main/java/com/diffplug/spotless/ClearGitConfig.java b/testlib/src/main/java/com/diffplug/spotless/ClearGitConfig.java new file mode 100644 index 0000000000..1655845831 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/ClearGitConfig.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.util.SystemReader; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@ExtendWith(ClearGitConfig.GitConfigExtension.class) +@ResourceLock(value = "GIT", mode = ResourceAccessMode.READ_WRITE) +public @interface ClearGitConfig { + + class GitConfigExtension implements BeforeEachCallback, AfterEachCallback { + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + for (var config : getConfigs()) { + config.clear(); + } + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + for (var config : getConfigs()) { + config.load(); + } + } + + private static List getConfigs() throws Exception { + var reader = SystemReader.getInstance(); + return List.of(reader.getUserConfig(), reader.getSystemConfig()); + } + } +} diff --git a/testlib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java b/testlib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java new file mode 100644 index 0000000000..53da84f4d8 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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 java.io.File; + +/** + * Formatter which is equal to itself, but not to any other Formatter. + */ +public class NeverUpToDateStep implements FormatterStep { + /** + * @param name + * The name of the formatter step + * @param function + * The function used by the formatter step + * @return A FormatterStep which will never report that it is up-to-date, because + * it is not equal to the serialized representation of itself. + */ + public static FormatterStep create( + String name, + SerializedFunction function) { + return new NeverUpToDateStep(name, function); + } + + private static final long serialVersionUID = 1L; + + private final String name; + private final SerializedFunction formatter; // initialized lazily + + NeverUpToDateStep(String name, SerializedFunction formatter) { + this.name = name; + this.formatter = formatter; + } + + @Override + public String getName() { + return name; + } + + @Override + public String format(String rawUnix, File file) throws Exception { + return formatter.apply(rawUnix); + } + + @Override + public void close() throws Exception { + if (formatter instanceof FormatterFunc.Closeable) { + ((FormatterFunc.Closeable) formatter).close(); + } + } +} diff --git a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java index b4a8bbae24..8d5cdff79f 100644 --- a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -27,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.UnaryOperator; @@ -49,17 +53,17 @@ public class ResourceHarness { File folderDontUseDirectly; /** Returns the root folder (canonicalized to fix OS X issue) */ - protected File rootFolder() { + public File rootFolder() { return Errors.rethrow().get(() -> folderDontUseDirectly.getCanonicalFile()); } /** Returns a new child of the root folder. */ - protected File newFile(String subpath) throws IOException { + public File newFile(String subpath) { return new File(rootFolder(), subpath); } /** Creates and returns a new child-folder of the root folder. */ - protected File newFolder(String subpath) throws IOException { + public File newFolder(String subpath) throws IOException { File targetDir = newFile(subpath); if (!targetDir.mkdir()) { throw new IOException("Failed to create " + targetDir); @@ -67,15 +71,50 @@ protected File newFolder(String subpath) throws IOException { return targetDir; } - protected String read(String path) throws IOException { + /** + * List the resources found in the specified path (directory) on the classpath + * @param path - absolute path using unix-style file separators (if it is relative, it will be made absolute before class path scanning) + * @return the list of resources in that directory on the classpath, unix-style file separators + * @throws IOException + */ + public List listTestResources(String path) throws IOException { + // add leading slash if required, otherwise resources won't be found + if (!path.startsWith("/")) { + path = path + "/"; + } + List filenames = new ArrayList<>(); + + try (InputStream in = ResourceHarness.class.getResourceAsStream(path)) { + if (in == null) { + if (new File(path).isAbsolute()) { + throw new RuntimeException(String.format("Resource not found in classpath: '%s'", path)); + } else { + throw new RuntimeException(String.format("Resource not found in classpath: '%s' - did you mean '/%1$s'?", path)); + } + } + try (BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String resource; + while ((resource = br.readLine()) != null) { + filenames.add(resource); + } + } + } + return filenames; + } + + public String relativeToRoot(String path) { + return new File(path).toPath().relativize(rootFolder().toPath()).toString(); + } + + public String read(String path) throws IOException { return read(newFile(path).toPath(), StandardCharsets.UTF_8); } - protected String read(Path path, Charset encoding) throws IOException { + public String read(Path path, Charset encoding) throws IOException { return new String(Files.readAllBytes(path), encoding); } - protected void replace(String path, String toReplace, String replaceWith) throws IOException { + public void replace(String path, String toReplace, String replaceWith) throws IOException { String before = read(path); String after = before.replace(toReplace, replaceWith); if (before.equals(after)) { @@ -85,16 +124,25 @@ protected void replace(String path, String toReplace, String replaceWith) throws } /** Returns the contents of the given file from the src/test/resources directory. */ - protected static String getTestResource(String filename) throws IOException { - URL url = ResourceHarness.class.getResource("/" + filename); - if (url == null) { - throw new IllegalArgumentException("No such resource " + filename); + public static String getTestResource(String filename) { + Optional resourceUrl = getTestResourceUrl(filename); + if (resourceUrl.isPresent()) { + return ThrowingEx.get(() -> LineEnding.toUnix(Resources.toString(resourceUrl.get(), StandardCharsets.UTF_8))); } - return Resources.toString(url, StandardCharsets.UTF_8); + throw new IllegalArgumentException("No such resource " + filename); + } + + public static boolean existsTestResource(String filename) { + return getTestResourceUrl(filename).isPresent(); + } + + private static Optional getTestResourceUrl(String filename) { + URL url = ResourceHarness.class.getResource("/" + filename); + return Optional.ofNullable(url); } /** Returns Files (in a temporary folder) which has the contents of the given file from the src/test/resources directory. */ - protected List createTestFiles(String... filenames) throws IOException { + public List createTestFiles(String... filenames) { List files = new ArrayList<>(filenames.length); for (String filename : filenames) { files.add(createTestFile(filename)); @@ -103,7 +151,7 @@ protected List createTestFiles(String... filenames) throws IOException { } /** Returns a File (in a temporary folder) which has the contents of the given file from the src/test/resources directory. */ - protected File createTestFile(String filename) throws IOException { + public File createTestFile(String filename) { return createTestFile(filename, UnaryOperator.identity()); } @@ -111,39 +159,22 @@ protected File createTestFile(String filename) throws IOException { * Returns a File (in a temporary folder) which has the contents, possibly processed, of the given file from the * src/test/resources directory. */ - protected File createTestFile(String filename, UnaryOperator fileContentsProcessor) throws IOException { + public File createTestFile(String filename, UnaryOperator fileContentsProcessor) { int lastSlash = filename.lastIndexOf('/'); String name = lastSlash >= 0 ? filename.substring(lastSlash) : filename; File file = newFile(name); file.getParentFile().mkdirs(); - Files.write(file.toPath(), fileContentsProcessor.apply(getTestResource(filename)).getBytes(StandardCharsets.UTF_8)); + ThrowingEx.run(() -> Files.write(file.toPath(), fileContentsProcessor.apply(getTestResource(filename)).getBytes(StandardCharsets.UTF_8))); return file; } - /** Reads the given resource from "before", applies the step, and makes sure the result is "after". */ - protected void assertOnResources(FormatterStep step, String unformattedPath, String expectedPath) throws Throwable { - assertOnResources(rawUnix -> step.format(rawUnix, new File("")), unformattedPath, expectedPath); - } - - /** Reads the given resource from "before", applies the step, and makes sure the result is "after". */ - protected void assertOnResources(FormatterFunc step, String unformattedPath, String expectedPath) throws Throwable { - String unformatted = LineEnding.toUnix(getTestResource(unformattedPath)); // unix-ified input - String formatted = step.apply(unformatted); - // no windows newlines - assertThat(formatted).doesNotContain("\r"); - - // unix-ify the test resource output in case git screwed it up - String expected = LineEnding.toUnix(getTestResource(expectedPath)); // unix-ified output - assertThat(formatted).isEqualTo(expected); - } - @CheckReturnValue - protected ReadAsserter assertFile(String path) throws IOException { + public ReadAsserter assertFile(String path) { return new ReadAsserter(newFile(path)); } @CheckReturnValue - protected ReadAsserter assertFile(File file) throws IOException { + public ReadAsserter assertFile(File file) { return new ReadAsserter(file); } @@ -176,7 +207,7 @@ public void matches(Consumer> conditions) } } - protected WriteAsserter setFile(String path) throws IOException { + public WriteAsserter setFile(String path) { return new WriteAsserter(newFile(path)); } @@ -188,21 +219,25 @@ private WriteAsserter(File file) { this.file = file; } - public File toLines(String... lines) throws IOException { + public File toLines(String... lines) { return toContent(String.join("\n", Arrays.asList(lines))); } - public File toContent(String content) throws IOException { + public File toContent(String content) { return toContent(content, StandardCharsets.UTF_8); } - public File toContent(String content, Charset charset) throws IOException { - Files.write(file.toPath(), content.getBytes(charset)); + public File toContent(String content, Charset charset) { + ThrowingEx.run(() -> { + Files.write(file.toPath(), content.getBytes(charset)); + }); return file; } - public File toResource(String path) throws IOException { - Files.write(file.toPath(), getTestResource(path).getBytes(StandardCharsets.UTF_8)); + public File toResource(String path) { + ThrowingEx.run(() -> { + Files.write(file.toPath(), getTestResource(path).getBytes(StandardCharsets.UTF_8)); + }); return file; } diff --git a/testlib/src/main/java/com/diffplug/spotless/SerializableEqualityTester.java b/testlib/src/main/java/com/diffplug/spotless/SerializableEqualityTester.java index 096edf62e2..726597a323 100644 --- a/testlib/src/main/java/com/diffplug/spotless/SerializableEqualityTester.java +++ b/testlib/src/main/java/com/diffplug/spotless/SerializableEqualityTester.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ public void areDifferentThan() { } @SuppressWarnings("unchecked") - private static T reserialize(T input) { + static T reserialize(T input) { byte[] asBytes = LazyForwardingEquality.toBytes(input); ByteArrayInputStream byteInput = new ByteArrayInputStream(asBytes); try (ObjectInputStream objectInput = new ObjectInputStream(byteInput)) { diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java index 8755f852d6..5b5a28717b 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,34 +19,20 @@ import java.io.File; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; import java.util.Arrays; -import java.util.Objects; -import java.util.function.Consumer; -import org.assertj.core.api.AbstractThrowableAssert; -import org.assertj.core.api.Assertions; +import com.diffplug.selfie.Selfie; +import com.diffplug.selfie.StringSelfie; /** An api for testing a {@code FormatterStep} that doesn't depend on the File path. DO NOT ADD FILE SUPPORT TO THIS, use {@link StepHarnessWithFile} if you need that. */ -public class StepHarness implements AutoCloseable { - private final FormatterFunc formatter; - - private StepHarness(FormatterFunc formatter) { - this.formatter = Objects.requireNonNull(formatter); +public class StepHarness extends StepHarnessBase { + private StepHarness(Formatter formatter, RoundTrip roundTrip) { + super(formatter, roundTrip); } /** Creates a harness for testing steps which don't depend on the file. */ public static StepHarness forStep(FormatterStep step) { - // We don't care if an individual FormatterStep is misbehaving on line-endings, because - // Formatter fixes that. No reason to care in tests either. It's likely to pop up when - // running tests on Windows from time-to-time - return new StepHarness(FormatterFunc.Closeable.ofDangerous( - () -> { - if (step instanceof FormatterStepImpl.Standard) { - ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); - } - }, - input -> LineEnding.toUnix(step.format(input, new File(""))))); + return forSteps(step); } /** Creates a harness for testing steps which don't depend on the file. */ @@ -55,61 +41,73 @@ public static StepHarness forSteps(FormatterStep... steps) { .steps(Arrays.asList(steps)) .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) .encoding(StandardCharsets.UTF_8) - .rootDir(Paths.get("")) .build()); } /** Creates a harness for testing a formatter whose steps don't depend on the file. */ public static StepHarness forFormatter(Formatter formatter) { - return new StepHarness(FormatterFunc.Closeable.ofDangerous( - formatter::close, - input -> formatter.compute(input, new File("")))); + return new StepHarness(formatter, RoundTrip.ASSERT_EQUAL); + } + + public static StepHarness forStepNoRoundtrip(FormatterStep step) { + return new StepHarness(Formatter.builder() + .steps(Arrays.asList(step)) + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .build(), RoundTrip.DONT_ROUNDTRIP); } /** Asserts that the given element is transformed as expected, and that the result is idempotent. */ - public StepHarness test(String before, String after) throws Exception { - String actual = formatter.apply(before); + public StepHarness test(String before, String after) { + String actual = formatter().compute(LineEnding.toUnix(before), new File("")); assertEquals(after, actual, "Step application failed"); return testUnaffected(after); } /** Asserts that the given element is idempotent w.r.t the step under test. */ - public StepHarness testUnaffected(String idempotentElement) throws Exception { - String actual = formatter.apply(idempotentElement); + public StepHarness testUnaffected(String idempotentElement) { + String actual = formatter().compute(LineEnding.toUnix(idempotentElement), new File("")); assertEquals(idempotentElement, actual, "Step is not idempotent"); return this; } /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarness testResource(String resourceBefore, String resourceAfter) throws Exception { + public StepHarness testResource(String resourceBefore, String resourceAfter) { String before = ResourceHarness.getTestResource(resourceBefore); String after = ResourceHarness.getTestResource(resourceAfter); - return test(before, after); + String actual = formatter().compute(LineEnding.toUnix(before), new File(resourceBefore)); + assertEquals(after, actual, "Step application failed"); + actual = formatter().compute(LineEnding.toUnix(after), new File(resourceAfter)); + assertEquals(after, actual, "Step is not idempotent"); + return this; } /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarness testResourceUnaffected(String resourceIdempotent) throws Exception { + public StepHarness testResourceUnaffected(String resourceIdempotent) { String idempotentElement = ResourceHarness.getTestResource(resourceIdempotent); return testUnaffected(idempotentElement); } - /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarness testResourceException(String resourceBefore, Consumer> exceptionAssertion) throws Exception { - return testException(ResourceHarness.getTestResource(resourceBefore), exceptionAssertion); + public StringSelfie expectLintsOfResource(String before) { + return expectLintsOf(ResourceHarness.getTestResource(before)); } - /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarness testException(String before, Consumer> exceptionAssertion) throws Exception { - Throwable t = assertThrows(Throwable.class, () -> formatter.apply(before)); - AbstractThrowableAssert abstractAssert = Assertions.assertThat(t); - exceptionAssertion.accept(abstractAssert); - return this; + public StringSelfie expectLintsOf(String before) { + LintState state = LintState.of(formatter(), Formatter.NO_FILE_SENTINEL, before.getBytes(formatter().getEncoding())); + return expectLintsOf(state, formatter()); } - @Override - public void close() { - if (formatter instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatter).close(); + static StringSelfie expectLintsOf(LintState state, Formatter formatter) { + String assertAgainst = state.asStringOneLine(Formatter.NO_FILE_SENTINEL, formatter); + String cleaned = assertAgainst.replace("NO_FILE_SENTINEL:", ""); + + int numLines = 1; + int lineEnding = cleaned.indexOf('\n'); + while (lineEnding != -1 && numLines < 10) { + ++numLines; + lineEnding = cleaned.indexOf('\n', lineEnding + 1); } + String toSnapshot = lineEnding == -1 ? cleaned : (cleaned.substring(0, lineEnding) + "\n(... and more)"); + return Selfie.expectSelfie(toSnapshot); } } diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java b/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java new file mode 100644 index 0000000000..d86021ef99 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2024 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 java.util.Objects; + +import org.assertj.core.api.Assertions; + +class StepHarnessBase implements AutoCloseable { + enum RoundTrip { + ASSERT_EQUAL, DONT_ASSERT_EQUAL, DONT_ROUNDTRIP + } + + private final Formatter formatter; + + protected StepHarnessBase(Formatter formatter, RoundTrip roundTrip) { + if (roundTrip == RoundTrip.DONT_ROUNDTRIP) { + this.formatter = Objects.requireNonNull(formatter); + return; + } + Formatter roundTripped = SerializableEqualityTester.reserialize(formatter); + if (roundTrip == RoundTrip.ASSERT_EQUAL) { + Assertions.assertThat(roundTripped).isEqualTo(formatter); + } + this.formatter = roundTripped; + } + + protected Formatter formatter() { + return formatter; + } + + @Override + public void close() { + formatter.close(); + } +} diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java b/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java index 89be961d04..519fe3037f 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,78 +18,94 @@ import static org.junit.jupiter.api.Assertions.*; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Objects; +import com.diffplug.selfie.StringSelfie; + /** An api for testing a {@code FormatterStep} that depends on the File path. */ -public class StepHarnessWithFile implements AutoCloseable { - private final FormatterFunc formatter; +public class StepHarnessWithFile extends StepHarnessBase { + private final ResourceHarness harness; - private StepHarnessWithFile(FormatterFunc formatter) { - this.formatter = Objects.requireNonNull(formatter); + private StepHarnessWithFile(ResourceHarness harness, Formatter formatter, RoundTrip roundTrip) { + super(formatter, roundTrip); + this.harness = Objects.requireNonNull(harness); } /** Creates a harness for testing steps which do depend on the file. */ - public static StepHarnessWithFile forStep(FormatterStep step) { - // We don't care if an individual FormatterStep is misbehaving on line-endings, because - // Formatter fixes that. No reason to care in tests either. It's likely to pop up when - // running tests on Windows from time-to-time - return new StepHarnessWithFile(FormatterFunc.Closeable.ofDangerous( - () -> { - if (step instanceof FormatterStepImpl.Standard) { - ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); - } - }, - new FormatterFunc() { - @Override - public String apply(String unix) throws Exception { - return apply(unix, new File("")); - } - - @Override - public String apply(String unix, File file) throws Exception { - return LineEnding.toUnix(step.format(unix, file)); - } - })); + public static StepHarnessWithFile forStep(ResourceHarness harness, FormatterStep step) { + return forFormatter(harness, Formatter.builder() + .encoding(StandardCharsets.UTF_8) + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .steps(Collections.singletonList(step)) + .build()); } /** Creates a harness for testing a formatter whose steps do depend on the file. */ - public static StepHarnessWithFile forFormatter(Formatter formatter) { - return new StepHarnessWithFile(FormatterFunc.Closeable.ofDangerous( - formatter::close, - input -> formatter.compute(input, new File("")))); + public static StepHarnessWithFile forFormatter(ResourceHarness harness, Formatter formatter) { + return new StepHarnessWithFile(harness, formatter, RoundTrip.ASSERT_EQUAL); } /** Asserts that the given element is transformed as expected, and that the result is idempotent. */ - public StepHarnessWithFile test(File file, String before, String after) throws Exception { - String actual = formatter.apply(before, file); + public StepHarnessWithFile test(String filename, String before, String after) { + File file = harness.setFile(filename).toContent(before); + String actual = formatter().compute(LineEnding.toUnix(before), file); assertEquals(after, actual, "Step application failed"); return testUnaffected(file, after); } /** Asserts that the given element is idempotent w.r.t the step under test. */ - public StepHarnessWithFile testUnaffected(File file, String idempotentElement) throws Exception { - String actual = formatter.apply(idempotentElement, file); + public StepHarnessWithFile testUnaffected(File file, String idempotentElement) { + String actual = formatter().compute(LineEnding.toUnix(idempotentElement), file); assertEquals(idempotentElement, actual, "Step is not idempotent"); return this; } + /** Asserts that the given element is idempotent w.r.t the step under test. */ + public StepHarnessWithFile testUnaffected(String file, String idempotentElement) { + return testUnaffected(harness.setFile(file).toContent(idempotentElement), idempotentElement); + } + /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarnessWithFile testResource(File file, String resourceBefore, String resourceAfter) throws Exception { - String before = ResourceHarness.getTestResource(resourceBefore); - String after = ResourceHarness.getTestResource(resourceAfter); - return test(file, before, after); + public StepHarnessWithFile testResource(String resourceBefore, String resourceAfter) { + return testResource(resourceBefore, resourceBefore, resourceAfter); + } + + public StepHarnessWithFile testResource(String filename, String resourceBefore, String resourceAfter) { + String contentBefore = ResourceHarness.getTestResource(resourceBefore); + return test(filename, contentBefore, ResourceHarness.getTestResource(resourceAfter)); } /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarnessWithFile testResourceUnaffected(File file, String resourceIdempotent) throws Exception { - String idempotentElement = ResourceHarness.getTestResource(resourceIdempotent); - return testUnaffected(file, idempotentElement); + public StepHarnessWithFile testResourceUnaffected(String resourceIdempotent) { + String contentBefore = ResourceHarness.getTestResource(resourceIdempotent); + File file = harness.setFile(resourceIdempotent).toContent(contentBefore); + return testUnaffected(file, contentBefore); + } + + public StringSelfie expectLintsOfResource(String resource) { + return expectLintsOfResource(resource, resource); + } + + public StringSelfie expectLintsOfResource(String filename, String resource) { + try { + File file = harness.setFile(filename).toResource(resource); + LintState state = LintState.of(formatter(), file); + return StepHarness.expectLintsOf(state, formatter()); + } catch (IOException e) { + throw new AssertionError(e); + } } - @Override - public void close() { - if (formatter instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatter).close(); + public StringSelfie expectLintsOfFileAndContent(String filename, String content) { + try { + File file = harness.setFile(filename).toContent(content); + LintState state = LintState.of(formatter(), file); + return StepHarness.expectLintsOf(state, formatter()); + } catch (IOException e) { + throw new AssertionError(e); } } } diff --git a/testlib/src/main/java/com/diffplug/spotless/TestProvisioner.java b/testlib/src/main/java/com/diffplug/spotless/TestProvisioner.java index 0d9ced4c9e..1ba0a745ee 100644 --- a/testlib/src/main/java/com/diffplug/spotless/TestProvisioner.java +++ b/testlib/src/main/java/com/diffplug/spotless/TestProvisioner.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,9 @@ import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ResolveException; import org.gradle.api.artifacts.dsl.RepositoryHandler; +import org.gradle.api.attributes.Attribute; import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.Category; import org.gradle.testfixtures.ProjectBuilder; import com.diffplug.common.base.Errors; @@ -51,10 +53,10 @@ public static Project gradleProject(File dir) { /** * Creates a Provisioner for the given repositories. - * + *

* The first time a project is created, there are ~7 seconds of configuration * which will go away for all subsequent runs. - * + *

* Every call to resolve will take about 1 second, even when all artifacts are resolved. */ private static Provisioner createWithRepositories(Consumer repoConfig) { @@ -70,7 +72,11 @@ private static Provisioner createWithRepositories(Consumer re config.setTransitive(withTransitives); config.setDescription(mavenCoords.toString()); 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)); + // TODO: This is a copy-paste from org.gradle.api.attributes.java.TargetJvmEnvironment which is added in Gradle 7.0, remove this once we drop support for Gradle 6.x. + // Add this attribute for resolving Guava dependency, see https://github.com/google/guava/issues/6801. + attr.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); }); try { return config.resolve(); diff --git a/testlib/src/main/java/com/diffplug/spotless/npm/EslintStyleGuide.java b/testlib/src/main/java/com/diffplug/spotless/npm/EslintStyleGuide.java new file mode 100644 index 0000000000..874ae1905d --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/npm/EslintStyleGuide.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023 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 java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +/** + * A helper class to create dev dependencies for eslint when using one of the popular styleguides in testing. + */ +public enum EslintStyleGuide { + TS_STANDARD_WITH_TYPESCRIPT("standard-with-typescript") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("@typescript-eslint/eslint-plugin", "^5.62.0"); + dependencies.put("@typescript-eslint/parser", "^5.62.0"); + dependencies.put("eslint-config-standard-with-typescript", "^36.0.1"); + dependencies.put("eslint-plugin-import", "^2.27.5"); + dependencies.put("eslint-plugin-n", "^16.0.1"); + dependencies.put("eslint-plugin-promise", "^6.1.1"); + return dependencies; + } + }, + TS_XO_TYPESCRIPT("xo-typescript") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("eslint-config-xo", "^0.43.1"); + dependencies.put("eslint-config-xo-typescript", "^1.0.0"); + return dependencies; + } + }, + JS_AIRBNB("airbnb") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("eslint-config-airbnb-base", "^15.0.0"); + dependencies.put("eslint-plugin-import", "^2.27.5"); + return dependencies; + } + }, + JS_GOOGLE("google") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("eslint-config-google", "^0.14.0"); + return dependencies; + } + }, + JS_STANDARD("standard") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("eslint-config-standard", "^17.1.0"); + dependencies.put("eslint-plugin-import", "^2.27.5"); + dependencies.put("eslint-plugin-n", "^16.0.1"); + dependencies.put("eslint-plugin-promise", "^6.1.1"); + return dependencies; + } + }, + JS_XO("xo") { + @Override + public @Nonnull Map devDependencies() { + Map dependencies = new LinkedHashMap<>(); + dependencies.put("eslint-config-xo", "^0.43.1"); + return dependencies; + } + }; + + private final String popularStyleGuideName; + + EslintStyleGuide(String popularStyleGuideName) { + this.popularStyleGuideName = popularStyleGuideName; + } + + public abstract @Nonnull Map devDependencies(); + + public static EslintStyleGuide fromNameOrNull(String popularStyleGuideName) { + for (EslintStyleGuide popularStyleGuide : EslintStyleGuide.values()) { + if (popularStyleGuide.popularStyleGuideName.equals(popularStyleGuideName)) { + return popularStyleGuide; + } + } + return null; + } + + public Map mergedWith(Map devDependencies) { + Map merged = new LinkedHashMap<>(devDependencies); + merged.putAll(devDependencies()); + return merged; + } + + public String asGradleMapStringMergedWith(Map devDependencies) { + return mergedWith(devDependencies).entrySet().stream() + .map(entry -> "'" + entry.getKey() + "': '" + entry.getValue() + "'") + .collect(Collectors.joining(", ", "[", "]")); + } + + public String asMavenXmlStringMergedWith(Map devDependencies) { + return mergedWith(devDependencies).entrySet().stream() + .map(entry -> String.format("%s%s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining("", "", "")); + } + +} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/BlackTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/BlackTest.java index 3d76499ea5..d0b34be10b 100644 --- a/testlib/src/main/java/com/diffplug/spotless/tag/BlackTest.java +++ b/testlib/src/main/java/com/diffplug/spotless/tag/BlackTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,5 +26,5 @@ @Target({TYPE, METHOD}) @Retention(RUNTIME) -@Tag("Black") +@Tag("black") public @interface BlackTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java new file mode 100644 index 0000000000..19923f4c6d --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022-2024 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.tag; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Tag("buf") +public @interface BufTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/ClangTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/ClangTest.java index 6eb9ee386e..e9f893bdfe 100644 --- a/testlib/src/main/java/com/diffplug/spotless/tag/ClangTest.java +++ b/testlib/src/main/java/com/diffplug/spotless/tag/ClangTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,5 +26,5 @@ @Target({TYPE, METHOD}) @Retention(RUNTIME) -@Tag("Clang") +@Tag("clang") public @interface ClangTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/GofmtTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/GofmtTest.java new file mode 100644 index 0000000000..6228a84895 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/tag/GofmtTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2024 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.tag; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Tag("gofmt") +public @interface GofmtTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/NpmTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/NpmTest.java index a1a6420b42..4f19ce3dfe 100644 --- a/testlib/src/main/java/com/diffplug/spotless/tag/NpmTest.java +++ b/testlib/src/main/java/com/diffplug/spotless/tag/NpmTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,5 +26,5 @@ @Target({TYPE, METHOD}) @Retention(RUNTIME) -@Tag("Npm") +@Tag("npm") public @interface NpmTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/ShfmtTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/ShfmtTest.java new file mode 100644 index 0000000000..e7998f6a83 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/tag/ShfmtTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 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.tag; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Tag("shfmt") +public @interface ShfmtTest {} diff --git a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/package-info.java b/testlib/src/main/java/selfie/SelfieSettings.java similarity index 66% rename from _ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/package-info.java rename to testlib/src/main/java/selfie/SelfieSettings.java index 770e126a1b..4a60c15dd0 100644 --- a/_ext/eclipse-wtp/src/main/java/com/diffplug/spotless/extra/eclipse/wtp/html/package-info.java +++ b/testlib/src/main/java/selfie/SelfieSettings.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Eclipse WTP HTML formatter helper */ -@ParametersAreNonnullByDefault -package com.diffplug.spotless.extra.eclipse.wtp.html; +package selfie; -import javax.annotation.ParametersAreNonnullByDefault; +import com.diffplug.selfie.junit5.SelfieSettingsAPI; + +/** https://selfie.dev/jvm/get-started#quickstart */ +public class SelfieSettings extends SelfieSettingsAPI { + @Override + public boolean getJavaDontUseTripleQuoteLiterals() { + return true; + } +} diff --git a/testlib/src/main/resources/biome/config/css-enabled.json b/testlib/src/main/resources/biome/config/css-enabled.json new file mode 100644 index 0000000000..692de05fe6 --- /dev/null +++ b/testlib/src/main/resources/biome/config/css-enabled.json @@ -0,0 +1,19 @@ +{ + "css": { + "linter": { + "enabled": true + }, + "formatter": { + "enabled": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 80, + "formatWithErrors": false + }, + "linter": { + "enabled": false + } + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/config/line-width-120.json b/testlib/src/main/resources/biome/config/line-width-120.json new file mode 100644 index 0000000000..8f14afa3f8 --- /dev/null +++ b/testlib/src/main/resources/biome/config/line-width-120.json @@ -0,0 +1,11 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 120, + "formatWithErrors": false + }, + "linter": { + "enabled": false + } + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/config/line-width-80.json b/testlib/src/main/resources/biome/config/line-width-80.json new file mode 100644 index 0000000000..5ec998bd97 --- /dev/null +++ b/testlib/src/main/resources/biome/config/line-width-80.json @@ -0,0 +1,11 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 80, + "formatWithErrors": false + }, + "linter": { + "enabled": false + } + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/css/fileAfter.css b/testlib/src/main/resources/biome/css/fileAfter.css new file mode 100644 index 0000000000..0fc2cd37a2 --- /dev/null +++ b/testlib/src/main/resources/biome/css/fileAfter.css @@ -0,0 +1,7 @@ +.foobar { + color: red; + font-weight: bold; + &.foobar--large { + font-size: 20px; + } +} diff --git a/testlib/src/main/resources/biome/css/fileBefore.css b/testlib/src/main/resources/biome/css/fileBefore.css new file mode 100644 index 0000000000..cfa88da947 --- /dev/null +++ b/testlib/src/main/resources/biome/css/fileBefore.css @@ -0,0 +1,12 @@ + .foobar { color:red;font-weight: bold; +&.foobar--large { + + + + font-size: 20px; + + + } + + + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/js/fileAfter.cjs b/testlib/src/main/resources/biome/js/fileAfter.cjs new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileAfter.cjs @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/biome/js/fileAfter.js b/testlib/src/main/resources/biome/js/fileAfter.js new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileAfter.js @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/biome/js/fileAfter.jsx b/testlib/src/main/resources/biome/js/fileAfter.jsx new file mode 100644 index 0000000000..313aceb6ed --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileAfter.jsx @@ -0,0 +1,3 @@ +export function Panel(cfg = {}) { + return

{1 + 2}
; +} diff --git a/testlib/src/main/resources/biome/js/fileAfter.mjs b/testlib/src/main/resources/biome/js/fileAfter.mjs new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileAfter.mjs @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/biome/js/fileBefore.cjs b/testlib/src/main/resources/biome/js/fileBefore.cjs new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileBefore.cjs @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/js/fileBefore.js b/testlib/src/main/resources/biome/js/fileBefore.js new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileBefore.js @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/js/fileBefore.jsx b/testlib/src/main/resources/biome/js/fileBefore.jsx new file mode 100644 index 0000000000..8e5d9834bc --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileBefore.jsx @@ -0,0 +1,4 @@ +export function Panel ( cfg={}){ + return (
{1+2}
+ ) ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/js/fileBefore.mjs b/testlib/src/main/resources/biome/js/fileBefore.mjs new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/biome/js/fileBefore.mjs @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/js/longLineAfter120.js b/testlib/src/main/resources/biome/js/longLineAfter120.js new file mode 100644 index 0000000000..68addc4b65 --- /dev/null +++ b/testlib/src/main/resources/biome/js/longLineAfter120.js @@ -0,0 +1 @@ +const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"]; diff --git a/testlib/src/main/resources/biome/js/longLineAfter80.js b/testlib/src/main/resources/biome/js/longLineAfter80.js new file mode 100644 index 0000000000..dbdbd157e9 --- /dev/null +++ b/testlib/src/main/resources/biome/js/longLineAfter80.js @@ -0,0 +1,13 @@ +const x = [ + "Hello", + "World", + "How", + "Are", + "You", + "Doing", + "Today", + "Such", + "A", + "Wondrous", + "Sunshine", +]; diff --git a/testlib/src/main/resources/biome/js/longLineBefore.js b/testlib/src/main/resources/biome/js/longLineBefore.js new file mode 100644 index 0000000000..fd59e429c2 --- /dev/null +++ b/testlib/src/main/resources/biome/js/longLineBefore.js @@ -0,0 +1 @@ +const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"]; \ No newline at end of file diff --git a/testlib/src/main/resources/biome/json/fileAfter.json b/testlib/src/main/resources/biome/json/fileAfter.json new file mode 100644 index 0000000000..468dac3297 --- /dev/null +++ b/testlib/src/main/resources/biome/json/fileAfter.json @@ -0,0 +1,5 @@ +{ + "a": [1, 2, 3], + "b": 9, + "c": null +} diff --git a/testlib/src/main/resources/biome/json/fileBefore.json b/testlib/src/main/resources/biome/json/fileBefore.json new file mode 100644 index 0000000000..77182284c7 --- /dev/null +++ b/testlib/src/main/resources/biome/json/fileBefore.json @@ -0,0 +1,7 @@ + { + "a":[1,2,3 + +], + "b":9, + "c" : null + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/json/package.json b/testlib/src/main/resources/biome/json/package.json new file mode 100644 index 0000000000..94e8f88de5 --- /dev/null +++ b/testlib/src/main/resources/biome/json/package.json @@ -0,0 +1,3 @@ +{ +"name": "test-package","version": "0.0.1" + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/json/packageAfter.json b/testlib/src/main/resources/biome/json/packageAfter.json new file mode 100644 index 0000000000..94e8f88de5 --- /dev/null +++ b/testlib/src/main/resources/biome/json/packageAfter.json @@ -0,0 +1,3 @@ +{ +"name": "test-package","version": "0.0.1" + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/json/packageBefore.json b/testlib/src/main/resources/biome/json/packageBefore.json new file mode 100644 index 0000000000..94e8f88de5 --- /dev/null +++ b/testlib/src/main/resources/biome/json/packageBefore.json @@ -0,0 +1,3 @@ +{ +"name": "test-package","version": "0.0.1" + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/jsonc/fileAfter.jsonc b/testlib/src/main/resources/biome/jsonc/fileAfter.jsonc new file mode 100644 index 0000000000..439d19ba1f --- /dev/null +++ b/testlib/src/main/resources/biome/jsonc/fileAfter.jsonc @@ -0,0 +1,6 @@ +{ + // some comment + "a": [1, 2, 3], + "b": 9, + "c": null +} diff --git a/testlib/src/main/resources/biome/jsonc/fileBefore.jsonc b/testlib/src/main/resources/biome/jsonc/fileBefore.jsonc new file mode 100644 index 0000000000..d7a2ca689f --- /dev/null +++ b/testlib/src/main/resources/biome/jsonc/fileBefore.jsonc @@ -0,0 +1,8 @@ + { + // some comment + "a":[1,2,3 + +], + "b":9, + "c" : null + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/ts/fileAfter.cts b/testlib/src/main/resources/biome/ts/fileAfter.cts new file mode 100644 index 0000000000..f854953234 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileAfter.cts @@ -0,0 +1,4 @@ +type Name = "World" | "Maven" | "Gradle"; +const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/biome/ts/fileAfter.mts b/testlib/src/main/resources/biome/ts/fileAfter.mts new file mode 100644 index 0000000000..e6563e3030 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileAfter.mts @@ -0,0 +1,4 @@ +export type Name = "World" | "Maven" | "Gradle"; +export const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/biome/ts/fileAfter.ts b/testlib/src/main/resources/biome/ts/fileAfter.ts new file mode 100644 index 0000000000..e6563e3030 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileAfter.ts @@ -0,0 +1,4 @@ +export type Name = "World" | "Maven" | "Gradle"; +export const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/biome/ts/fileAfter.tsx b/testlib/src/main/resources/biome/ts/fileAfter.tsx new file mode 100644 index 0000000000..15ef316142 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileAfter.tsx @@ -0,0 +1,7 @@ +export interface Cfg { + classname: string; + message: T; +} +const Panel = (cfg: Cfg): JSX.Element => { + return
{String(cfg.message)}
; +}; diff --git a/testlib/src/main/resources/biome/ts/fileBefore.cts b/testlib/src/main/resources/biome/ts/fileBefore.cts new file mode 100644 index 0000000000..d4304287c0 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileBefore.cts @@ -0,0 +1,4 @@ +type Name = "World" | "Maven"|"Gradle"; +const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/ts/fileBefore.mts b/testlib/src/main/resources/biome/ts/fileBefore.mts new file mode 100644 index 0000000000..96837762a3 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileBefore.mts @@ -0,0 +1,5 @@ +export + type Name = "World" | "Maven"|"Gradle"; +export const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/ts/fileBefore.ts b/testlib/src/main/resources/biome/ts/fileBefore.ts new file mode 100644 index 0000000000..96837762a3 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileBefore.ts @@ -0,0 +1,5 @@ +export + type Name = "World" | "Maven"|"Gradle"; +export const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/biome/ts/fileBefore.tsx b/testlib/src/main/resources/biome/ts/fileBefore.tsx new file mode 100644 index 0000000000..38f24f8440 --- /dev/null +++ b/testlib/src/main/resources/biome/ts/fileBefore.tsx @@ -0,0 +1,8 @@ +export interface Cfg{ +classname:string, +message:T, +} +const Panel = ( cfg:Cfg):JSX.Element =>{ + return (
{String(cfg.message)}
+ ) ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/combined/issue1679.clean b/testlib/src/main/resources/combined/issue1679.clean new file mode 100644 index 0000000000..af86db275e --- /dev/null +++ b/testlib/src/main/resources/combined/issue1679.clean @@ -0,0 +1,106 @@ +/* + * Copyright 2021-2022 Creek Contributors (https://github.com/creek-service) + * + * 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 io.github.creek.service.basic.kafka.streams.demo.services; + +// formatting:off +// begin-snippet: includes-1 +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicConfigBuilder.withPartitions; +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicDescriptors.inputTopic; +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicDescriptors.outputTopic; +// end-snippet +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +// begin-snippet: includes-2 +import org.creekservice.api.kafka.metadata.OwnedKafkaTopicInput; +import org.creekservice.api.kafka.metadata.OwnedKafkaTopicOutput; +// end-snippet +import org.creekservice.api.platform.metadata.ComponentInput; +import org.creekservice.api.platform.metadata.ComponentInternal; +import org.creekservice.api.platform.metadata.ComponentOutput; +import org.creekservice.api.platform.metadata.ServiceDescriptor; +// formatting:on + +// begin-snippet: class-name +public final class HandleOccurrenceServiceDescriptor implements ServiceDescriptor { + // end-snippet + private static final List INPUTS = new ArrayList<>(); + private static final List INTERNALS = new ArrayList<>(); + private static final List OUTPUTS = new ArrayList<>(); + + // formatting:off +// begin-snippet: topic-resources + // Define the tweet-text input topic, conceptually owned by this service: + public static final OwnedKafkaTopicInput TweetTextStream = + register( + inputTopic( + "twitter.tweet.text", // Topic name + Long.class, // Topic key: Tweet id + String.class, // Topic value: Tweet text + withPartitions(5))); // Topic config + + // Define the output topic, again conceptually owned by this service: + public static final OwnedKafkaTopicOutput TweetHandleUsageStream = + register(outputTopic( + "twitter.handle.usage", + String.class, // Twitter handle + Integer.class, // Usage count + withPartitions(6) + .withRetentionTime(Duration.ofHours(12)) + )); +// end-snippet +// formatting:on + + public HandleOccurrenceServiceDescriptor() {} + + @Override + public String dockerImage() { + return "ghcr.io/creek-service/basic-kafka-streams-demo-handle-occurrence-service"; + } + + @Override + public Collection inputs() { + return List.copyOf(INPUTS); + } + + @Override + public Collection internals() { + return List.copyOf(INTERNALS); + } + + @Override + public Collection outputs() { + return List.copyOf(OUTPUTS); + } + + private static T register(final T input) { + INPUTS.add(input); + return input; + } + + // Uncomment if needed: + // private static T register(final T internal) { + // INTERNALS.add(internal); + // return internal; + // } + + private static T register(final T output) { + OUTPUTS.add(output); + return output; + } +} diff --git a/testlib/src/main/resources/combined/issue1679.dirty b/testlib/src/main/resources/combined/issue1679.dirty new file mode 100644 index 0000000000..70754cef6c --- /dev/null +++ b/testlib/src/main/resources/combined/issue1679.dirty @@ -0,0 +1,104 @@ +/* + * Copyright 2021-2022 Creek Contributors (https://github.com/creek-service) + * + * 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 io.github.creek.service.basic.kafka.streams.demo.services; + +// formatting:off +// begin-snippet: includes-1 +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicConfigBuilder.withPartitions; +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicDescriptors.inputTopic; +import static io.github.creek.service.basic.kafka.streams.demo.internal.TopicDescriptors.outputTopic; +// end-snippet +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +// begin-snippet: includes-2 +import org.creekservice.api.kafka.metadata.OwnedKafkaTopicInput; +import org.creekservice.api.kafka.metadata.OwnedKafkaTopicOutput; +// end-snippet +import org.creekservice.api.platform.metadata.ComponentInput; +import org.creekservice.api.platform.metadata.ComponentInternal; +import org.creekservice.api.platform.metadata.ComponentOutput; +import org.creekservice.api.platform.metadata.ServiceDescriptor; +// formatting:on + +// begin-snippet: class-name +public final class HandleOccurrenceServiceDescriptor implements ServiceDescriptor { + // end-snippet + private static final List INPUTS = new ArrayList<>(); + private static final List INTERNALS = new ArrayList<>(); + private static final List OUTPUTS = new ArrayList<>(); + + // formatting:off +// begin-snippet: topic-resources + // Define the tweet-text input topic, conceptually owned by this service: + public static final OwnedKafkaTopicInput TweetTextStream = + register( + inputTopic( + "twitter.tweet.text", // Topic name + Long.class, // Topic key: Tweet id + String.class, // Topic value: Tweet text + withPartitions(5))); // Topic config + + // Define the output topic, again conceptually owned by this service: + public static final OwnedKafkaTopicOutput TweetHandleUsageStream = + register(outputTopic( + "twitter.handle.usage", + String.class, // Twitter handle + Integer.class, // Usage count + withPartitions(6) + .withRetentionTime(Duration.ofHours(12)) + )); +// end-snippet +// formatting:on + + public HandleOccurrenceServiceDescriptor() {} + + @Override + public String dockerImage() { + return "ghcr.io/creek-service/basic-kafka-streams-demo-handle-occurrence-service"; + } + + @Override + public Collection inputs() { return List.copyOf(INPUTS); } + + @Override + public Collection internals() { + return List.copyOf(INTERNALS); + } + + @Override + public Collection outputs() { + return List.copyOf(OUTPUTS); + } + + private static T register(final T input) { + INPUTS.add(input); + return input; + } + + // Uncomment if needed: + // private static T register(final T internal) { + // INTERNALS.add(internal); + // return internal; + // } + + private static T register(final T output) { + OUTPUTS.add(output); + return output; + } +} \ No newline at end of file diff --git a/testlib/src/main/resources/gherkin/complex_backgroundAfter.feature b/testlib/src/main/resources/gherkin/complex_backgroundAfter.feature new file mode 100644 index 0000000000..eb4d4de89a --- /dev/null +++ b/testlib/src/main/resources/gherkin/complex_backgroundAfter.feature @@ -0,0 +1,24 @@ +Feature: Complex background + We want to ensure PickleStep all have different IDs + + Background: a simple background + Given the minimalism inside a background + + Scenario: minimalistic + Given the minimalism + + Scenario: also minimalistic + Given the minimalism + + Rule: My Rule + + Background: + Given a rule background step + + Scenario: with examples + Given the minimalism + + Examples: + | value | + | 1 | + | 2 | diff --git a/testlib/src/main/resources/gherkin/complex_backgroundBefore.feature b/testlib/src/main/resources/gherkin/complex_backgroundBefore.feature new file mode 100644 index 0000000000..fde7c94d9b --- /dev/null +++ b/testlib/src/main/resources/gherkin/complex_backgroundBefore.feature @@ -0,0 +1,24 @@ +Feature: Complex background + We want to ensure PickleStep all have different IDs + + Background: a simple background + Given the minimalism inside a background + + Scenario: minimalistic + Given the minimalism + + Scenario: also minimalistic + Given the minimalism + + Rule: My Rule + + Background: + Given a rule background step + + Scenario: with examples + Given the minimalism + + Examples: + | value | + | 1 | + | 2 | diff --git a/testlib/src/main/resources/gherkin/descriptionsAfter.feature b/testlib/src/main/resources/gherkin/descriptionsAfter.feature new file mode 100644 index 0000000000..897133527d --- /dev/null +++ b/testlib/src/main/resources/gherkin/descriptionsAfter.feature @@ -0,0 +1,55 @@ +Feature: Descriptions everywhere + This is a single line description + + Scenario: two lines + This description + has two lines and indented with two spaces + + Given the minimalism + + Scenario: without indentation +This is a description without indentation + + Given the minimalism + + Scenario: empty lines in the middle + This description + + has an empty line in the middle + + Given the minimalism + + Scenario: empty lines around + This description + has an empty lines around + + Given the minimalism + + Scenario: comment after description + This description + has a comment after + + # this is a comment + Given the minimalism + + Scenario: comment right after description + This description + has a comment right after + + # this is another comment + Given the minimalism + + Scenario: description with escaped docstring separator + This description has an \"\"\" (escaped docstring sparator) + + Given the minimalism + + Scenario Outline: scenario outline with a description +This is a scenario outline description + + Given the minimalism + + Examples: examples with description +This is an examples description + | foo | + | bar | diff --git a/testlib/src/main/resources/gherkin/descriptionsBefore.feature b/testlib/src/main/resources/gherkin/descriptionsBefore.feature new file mode 100644 index 0000000000..690ae3a754 --- /dev/null +++ b/testlib/src/main/resources/gherkin/descriptionsBefore.feature @@ -0,0 +1,51 @@ +Feature: Descriptions everywhere + This is a single line description + + Scenario: two lines + This description + has two lines and indented with two spaces + Given the minimalism + +Scenario: without indentation +This is a description without indentation + Given the minimalism + + Scenario: empty lines in the middle + This description + + has an empty line in the middle + Given the minimalism + + Scenario: empty lines around + + This description + has an empty lines around + + Given the minimalism + + Scenario: comment after description + This description + has a comment after + +# this is a comment + Given the minimalism + + Scenario: comment right after description + This description + has a comment right after + # this is another comment + Given the minimalism + + Scenario: description with escaped docstring separator + This description has an \"\"\" (escaped docstring sparator) + + Given the minimalism + + Scenario Outline: scenario outline with a description +This is a scenario outline description + Given the minimalism + + Examples: examples with description +This is an examples description + | foo | + | bar | diff --git a/testlib/src/main/resources/gherkin/emptyAfter.feature b/testlib/src/main/resources/gherkin/emptyAfter.feature new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testlib/src/main/resources/gherkin/emptyBefore.feature b/testlib/src/main/resources/gherkin/emptyBefore.feature new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/testlib/src/main/resources/gherkin/emptyBefore.feature @@ -0,0 +1 @@ + diff --git a/testlib/src/main/resources/gherkin/invalidGherkinAfter.feature b/testlib/src/main/resources/gherkin/invalidGherkinAfter.feature new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testlib/src/main/resources/gherkin/invalidGherkinBefore.feature b/testlib/src/main/resources/gherkin/invalidGherkinBefore.feature new file mode 100644 index 0000000000..665bf227ca --- /dev/null +++ b/testlib/src/main/resources/gherkin/invalidGherkinBefore.feature @@ -0,0 +1,9 @@ + +invalid line here + +Feature: Multiple parser errors + + Scenario: minimalistic + Given the minimalism + + another invalid line here diff --git a/testlib/src/main/resources/gherkin/minimalAfter.feature b/testlib/src/main/resources/gherkin/minimalAfter.feature new file mode 100644 index 0000000000..9a62d86f80 --- /dev/null +++ b/testlib/src/main/resources/gherkin/minimalAfter.feature @@ -0,0 +1,4 @@ +Feature: Minimal + + Scenario: minimalistic + Given the minimalism diff --git a/testlib/src/main/resources/gherkin/minimalAfter0Spaces.feature b/testlib/src/main/resources/gherkin/minimalAfter0Spaces.feature new file mode 100644 index 0000000000..aa94821bbc --- /dev/null +++ b/testlib/src/main/resources/gherkin/minimalAfter0Spaces.feature @@ -0,0 +1,4 @@ +Feature: Minimal + +Scenario: minimalistic +Given the minimalism diff --git a/testlib/src/main/resources/gherkin/minimalAfter6Spaces.feature b/testlib/src/main/resources/gherkin/minimalAfter6Spaces.feature new file mode 100644 index 0000000000..cbad451b45 --- /dev/null +++ b/testlib/src/main/resources/gherkin/minimalAfter6Spaces.feature @@ -0,0 +1,4 @@ +Feature: Minimal + + Scenario: minimalistic + Given the minimalism diff --git a/testlib/src/main/resources/gherkin/minimalBefore.feature b/testlib/src/main/resources/gherkin/minimalBefore.feature new file mode 100644 index 0000000000..a474cf1418 --- /dev/null +++ b/testlib/src/main/resources/gherkin/minimalBefore.feature @@ -0,0 +1,3 @@ +Feature: Minimal +Scenario: minimalistic +Given the minimalism diff --git a/testlib/src/main/resources/gherkin/notGherkinAfter.feature b/testlib/src/main/resources/gherkin/notGherkinAfter.feature new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testlib/src/main/resources/gherkin/notGherkinBefore.feature b/testlib/src/main/resources/gherkin/notGherkinBefore.feature new file mode 100644 index 0000000000..517db4bd1e --- /dev/null +++ b/testlib/src/main/resources/gherkin/notGherkinBefore.feature @@ -0,0 +1 @@ +not gherkin diff --git a/testlib/src/main/resources/gherkin/rule_with_tagAfter.feature b/testlib/src/main/resources/gherkin/rule_with_tagAfter.feature new file mode 100644 index 0000000000..bba6b044ac --- /dev/null +++ b/testlib/src/main/resources/gherkin/rule_with_tagAfter.feature @@ -0,0 +1,19 @@ +@tag_feature +Feature: Some tagged rules + + Rule: Untagged rule + The untagged rule description + + Scenario: Scenario with only a feature tag + Given a + + @tag_rule + Rule: Tagged rule + The tagged rule description + + Scenario: Scenario with feature and rule tags + Given b + + @tag_scenario + Scenario: Scenario with feature, rule and scenario tags + Given b diff --git a/testlib/src/main/resources/gherkin/rule_with_tagBefore.feature b/testlib/src/main/resources/gherkin/rule_with_tagBefore.feature new file mode 100644 index 0000000000..bba6b044ac --- /dev/null +++ b/testlib/src/main/resources/gherkin/rule_with_tagBefore.feature @@ -0,0 +1,19 @@ +@tag_feature +Feature: Some tagged rules + + Rule: Untagged rule + The untagged rule description + + Scenario: Scenario with only a feature tag + Given a + + @tag_rule + Rule: Tagged rule + The tagged rule description + + Scenario: Scenario with feature and rule tags + Given b + + @tag_scenario + Scenario: Scenario with feature, rule and scenario tags + Given b diff --git a/testlib/src/main/resources/go/gofmt/go.clean b/testlib/src/main/resources/go/gofmt/go.clean new file mode 100644 index 0000000000..321185cd76 --- /dev/null +++ b/testlib/src/main/resources/go/gofmt/go.clean @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func split(sum int) (x int, y int) { + x = sum * 4 / 9 + y = sum - x + return +} + +func main() { + fmt.Println( + split(17)) +} diff --git a/testlib/src/main/resources/go/gofmt/go.dirty b/testlib/src/main/resources/go/gofmt/go.dirty new file mode 100644 index 0000000000..f1f9adaca7 --- /dev/null +++ b/testlib/src/main/resources/go/gofmt/go.dirty @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func split (sum int) (x int , y int) { + x = sum * 4 / 9 + y = sum - x + return +} + +func main() { + fmt.Println( + split(17)) +} diff --git a/testlib/src/main/resources/groovy/greclipse/format/SomeClass.fixed b/testlib/src/main/resources/groovy/greclipse/format/SomeClass.fixed new file mode 100644 index 0000000000..8c93ca4ad0 --- /dev/null +++ b/testlib/src/main/resources/groovy/greclipse/format/SomeClass.fixed @@ -0,0 +1,12 @@ +package com.somepackage + +class SomeClass { + + def func(parm) { + """ + ${parm == null ? "" : "parm"} + ${parm == null ? "" : "parm"} + + """ + } +} diff --git a/testlib/src/main/resources/groovy/greclipse/format/SomeClass.test b/testlib/src/main/resources/groovy/greclipse/format/SomeClass.test new file mode 100644 index 0000000000..62d4df0fc5 --- /dev/null +++ b/testlib/src/main/resources/groovy/greclipse/format/SomeClass.test @@ -0,0 +1,12 @@ +package com.somepackage + +class SomeClass { + + def func(parm) { + """ + ${parm == null ? "" : "$parm"} + ${parm == null ? "" : "$parm"} + + """ + } +} diff --git a/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolons.test b/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolons.test new file mode 100644 index 0000000000..b66af4e820 --- /dev/null +++ b/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolons.test @@ -0,0 +1,10 @@ +import mylib.Unused; +import mylib.UsedB; +import mylib.UsedA; + +public class SomeClass { +System.out.println("hello"); +UsedB.someMethod(); +UsedA.someMethod(); +} +} \ No newline at end of file diff --git a/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test b/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test new file mode 100644 index 0000000000..434addfa68 --- /dev/null +++ b/testlib/src/main/resources/groovy/removeSemicolons/GroovyCodeWithSemicolonsFormatted.test @@ -0,0 +1,10 @@ +import mylib.Unused +import mylib.UsedB +import mylib.UsedA + +public class SomeClass { +System.out.println("hello") +UsedB.someMethod() +UsedA.someMethod() +} +} \ No newline at end of file diff --git a/testlib/src/main/resources/indent/IndentedMixed.test b/testlib/src/main/resources/indent/IndentedMixed.test new file mode 100644 index 0000000000..37c108c1df --- /dev/null +++ b/testlib/src/main/resources/indent/IndentedMixed.test @@ -0,0 +1,37 @@ +package com.github.youribonnaffe.gradle.format; + +import java.util.function.Function; + +/** + * Test class. + */ +public class Java8Test { + /** + * Test method. + */ + public void doStuff() throws Exception { + Function example = Integer::parseInt; + example.andThen(val -> { + return val + 2; + } ); + SimpleEnum val = SimpleEnum.A; + switch (val) { + case A: + break; + case B: + break; + case C: + break; + default: + throw new Exception(); + } + } + + /** Test enum + * with weirdly formatted javadoc + * which IndentStep should not change + */ + public enum SimpleEnum { + A, B, C; + } +} diff --git a/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.clean.test b/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.clean.test new file mode 100644 index 0000000000..8bacfa58db --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.clean.test @@ -0,0 +1,8 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return "hardcoded".equals(input); + } +} diff --git a/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.dirty.test b/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.dirty.test new file mode 100644 index 0000000000..3a1e074c91 --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/LiteralsFirstInComparisons.dirty.test @@ -0,0 +1,8 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return input.equals("hardcoded"); + } +} diff --git a/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyLiteralsFirst.test b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyLiteralsFirst.test new file mode 100644 index 0000000000..629d24504b --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyLiteralsFirst.test @@ -0,0 +1,14 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +import java.util.Optional; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return "hardcoded".equals(input); + } + + public boolean isPresent(Optional optional) { + return !optional.isEmpty(); + } +} diff --git a/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyOptionalIsPresent.test b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyOptionalIsPresent.test new file mode 100644 index 0000000000..0829602dc1 --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.onlyOptionalIsPresent.test @@ -0,0 +1,14 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +import java.util.Optional; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return input.equals("hardcoded"); + } + + public boolean isPresent(Optional optional) { + return optional.isPresent(); + } +} diff --git a/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.test b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.test new file mode 100644 index 0000000000..318e1efa15 --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/MultipleMutators.clean.test @@ -0,0 +1,14 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +import java.util.Optional; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return "hardcoded".equals(input); + } + + public boolean isPresent(Optional optional) { + return optional.isPresent(); + } +} diff --git a/testlib/src/main/resources/java/cleanthat/MultipleMutators.dirty.test b/testlib/src/main/resources/java/cleanthat/MultipleMutators.dirty.test new file mode 100644 index 0000000000..8ac230cabc --- /dev/null +++ b/testlib/src/main/resources/java/cleanthat/MultipleMutators.dirty.test @@ -0,0 +1,14 @@ +package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me; + +import java.util.Optional; + +public class LiteralsFirstInComparisonsCases { + + public boolean isHardcoded(String input) { + return input.equals("hardcoded"); + } + + public boolean isPresent(Optional optional) { + return !optional.isEmpty(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/AbstractType.clean b/testlib/src/main/resources/java/eclipse/AbstractType.clean new file mode 100644 index 0000000000..314dbac1ac --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/AbstractType.clean @@ -0,0 +1,41 @@ +package test; + +public abstract class AbstractType { + + private String _typeName; + + AbstractType(String typeName) { + _typeName = typeName; + } + + private String _type() { + String name = getClass().getSimpleName(); + return name.endsWith("Type") + ? name.substring(0, getClass().getSimpleName().length() - 4) + : name; + } + + AbstractType argument() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + @Override + public boolean equals(Object another) { + if (this == another) { + return true; + } + return another instanceof AbstractType t + && _typeName.equals(t._typeName); + } + + @Override + public int hashCode() { + return _typeName.hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(typeName)"; + } + +} diff --git a/testlib/src/main/resources/java/eclipse/AbstractType.test b/testlib/src/main/resources/java/eclipse/AbstractType.test new file mode 100644 index 0000000000..d4753b2cfc --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/AbstractType.test @@ -0,0 +1,54 @@ +package test; + + + + +public abstract class AbstractType { + + + private String _typeName; + + AbstractType(String typeName) { + _typeName = typeName; + } + + + + private String _type() { + String name = getClass().getSimpleName(); + return name.endsWith("Type") + ? name.substring(0, getClass().getSimpleName().length() - 4) + : name; + } + + AbstractType argument() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + + @Override + public boolean equals(Object another) { + if (this == another) { + return true; + } + return another instanceof AbstractType t + && _typeName.equals(t._typeName); + } + + + + @Override + public int hashCode() { + return _typeName.hashCode(); + } + + + + + @Override + public String toString() { + return getClass().getSimpleName() + "(typeName)"; + } + + +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.clean b/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.clean new file mode 100644 index 0000000000..d1000f3b19 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.clean @@ -0,0 +1,50 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:doNotSortFields = true +public class SortExample { + public static final int Z; + private static final int A; + static final int B = 1; + protected static final int C = 2; + static { + Z = 9; + A = 0; + } + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } + class Nested { + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() { + } + public void b() { + } + private void c() { + } + protected void d() { + } + void z() { + } + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.test b/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.test new file mode 100644 index 0000000000..9af5447b7f --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localDoNotSortFields.test @@ -0,0 +1,38 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:doNotSortFields = true +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() {} + private static void s2() {} + public static void s3() {} + protected static void s4() {} + SortExample(int b, int c) {} + SortExample(int a) {} + SortExample() {} + class Nested { + void z() {} + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() {} + private void c() {} + protected void d() {} + public void b() {} + } + public static final int Z; + private static final int A; + static final int B =1; + protected static final int C= 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.clean b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.clean new file mode 100644 index 0000000000..050e6b47e8 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.clean @@ -0,0 +1,50 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:enabled = false +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + SortExample(int b, int c) { + } + SortExample(int a) { + } + SortExample() { + } + class Nested { + void z() { + } + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() { + } + private void c() { + } + protected void d() { + } + public void b() { + } + } + public static final int Z; + private static final int A; + static final int B = 1; + protected static final int C = 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.test b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.test new file mode 100644 index 0000000000..9c544d3250 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledFalse.test @@ -0,0 +1,38 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:enabled = false +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() {} + private static void s2() {} + public static void s3() {} + protected static void s4() {} + SortExample(int b, int c) {} + SortExample(int a) {} + SortExample() {} + class Nested { + void z() {} + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() {} + private void c() {} + protected void d() {} + public void b() {} + } + public static final int Z; + private static final int A; + static final int B =1; + protected static final int C= 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.clean b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.clean new file mode 100644 index 0000000000..565c5ca99e --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.clean @@ -0,0 +1,50 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:enabled = true +public class SortExample { + class Nested { + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() { + } + public void b() { + } + private void c() { + } + protected void d() { + } + void z() { + } + } + public static final int Z; + private static final int A; + static final int B = 1; + protected static final int C = 2; + static { + Z = 9; + A = 0; + } + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.test b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.test new file mode 100644 index 0000000000..40735750ee --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localEnabledTrue.test @@ -0,0 +1,38 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:enabled = true +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() {} + private static void s2() {} + public static void s3() {} + protected static void s4() {} + SortExample(int b, int c) {} + SortExample(int a) {} + SortExample() {} + class Nested { + void z() {} + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() {} + private void c() {} + protected void d() {} + public void b() {} + } + public static final int Z; + private static final int A; + static final int B =1; + protected static final int C= 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.clean b/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.clean new file mode 100644 index 0000000000..6224e90517 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.clean @@ -0,0 +1,50 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:sortByVisibility = false +public class SortExample { + private static final int A; + static final int B = 1; + protected static final int C = 2; + public static final int Z; + static { + Z = 9; + A = 0; + } + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + final boolean _1 = false; + final int a = 1; + public final int b = 1; + private final int c = 1; + protected final int d = 1; + protected final int e = 1; + public final int f = 1; + final int z = 1; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } + class Nested { + public static final String B = "B"; + private static final String C = "C"; + protected static final String D = "D"; + void a() { + } + public void b() { + } + private void c() { + } + protected void d() { + } + void z() { + } + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.test b/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.test new file mode 100644 index 0000000000..e3cfb77617 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.localSortByVisibility.test @@ -0,0 +1,38 @@ +package com.diffplug.spotless.extra.glue.jdt; + +// @SortMembers:sortByVisibility = false +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() {} + private static void s2() {} + public static void s3() {} + protected static void s4() {} + SortExample(int b, int c) {} + SortExample(int a) {} + SortExample() {} + class Nested { + void z() {} + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() {} + private void c() {} + protected void d() {} + public void b() {} + } + public static final int Z; + private static final int A; + static final int B =1; + protected static final int C= 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.sortMembers.clean b/testlib/src/main/resources/java/eclipse/SortExample.sortMembers.clean new file mode 100644 index 0000000000..e6f3c1dc0e --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.sortMembers.clean @@ -0,0 +1,49 @@ +package com.diffplug.spotless.extra.glue.jdt; + +public class SortExample { + private static final int A; + static final int B = 1; + protected static final int C = 2; + public static final int Z; + static { + Z = 9; + A = 0; + } + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + final boolean _1 = false; + final int a = 1; + public final int b = 1; + private final int c = 1; + protected final int d = 1; + protected final int e = 1; + public final int f = 1; + final int z = 1; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } + class Nested { + public static final String B = "B"; + private static final String C = "C"; + protected static final String D = "D"; + void a() { + } + public void b() { + } + private void c() { + } + protected void d() { + } + void z() { + } + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.sortMembersByVisibility.clean b/testlib/src/main/resources/java/eclipse/SortExample.sortMembersByVisibility.clean new file mode 100644 index 0000000000..e5d1528ba1 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.sortMembersByVisibility.clean @@ -0,0 +1,49 @@ +package com.diffplug.spotless.extra.glue.jdt; + +public class SortExample { + public static final int Z; + protected static final int C = 2; + static final int B = 1; + private static final int A; + static { + Z = 9; + A = 0; + } + public static void s3() { + } + protected static void s4() { + } + static void s1() { + } + private static void s2() { + } + public final int b = 1; + public final int f = 1; + protected final int d = 1; + protected final int e = 1; + final boolean _1 = false; + final int a = 1; + final int z = 1; + private final int c = 1; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } + class Nested { + public static final String B = "B"; + protected static final String D = "D"; + private static final String C = "C"; + public void b() { + } + protected void d() { + } + void a() { + } + void z() { + } + private void c() { + } + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.sortMembersNoFields.clean b/testlib/src/main/resources/java/eclipse/SortExample.sortMembersNoFields.clean new file mode 100644 index 0000000000..499f0ef413 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.sortMembersNoFields.clean @@ -0,0 +1,49 @@ +package com.diffplug.spotless.extra.glue.jdt; + +public class SortExample { + public static final int Z; + private static final int A; + static final int B = 1; + protected static final int C = 2; + static { + Z = 9; + A = 0; + } + static void s1() { + } + private static void s2() { + } + public static void s3() { + } + protected static void s4() { + } + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + SortExample() { + } + SortExample(int a) { + } + SortExample(int b, int c) { + } + class Nested { + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() { + } + public void b() { + } + private void c() { + } + protected void d() { + } + void z() { + } + } +} diff --git a/testlib/src/main/resources/java/eclipse/SortExample.test b/testlib/src/main/resources/java/eclipse/SortExample.test new file mode 100644 index 0000000000..421248e7b5 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/SortExample.test @@ -0,0 +1,37 @@ +package com.diffplug.spotless.extra.glue.jdt; + +public class SortExample { + final int z = 1; + final int a = 1; + private final int c = 1; + protected final int e = 1; + protected final int d = 1; + public final int f = 1; + public final int b = 1; + final boolean _1 = false; + static void s1() {} + private static void s2() {} + public static void s3() {} + protected static void s4() {} + SortExample(int b, int c) {} + SortExample(int a) {} + SortExample() {} + class Nested { + void z() {} + private static final String C = "C"; + protected static final String D = "D"; + public static final String B = "B"; + void a() {} + private void c() {} + protected void d() {} + public void b() {} + } + public static final int Z; + private static final int A; + static final int B =1; + protected static final int C= 2; + static { + Z = 9; + A = 0; + } +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersInput.test new file mode 100644 index 0000000000..13546f9a84 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersInput.test @@ -0,0 +1,63 @@ +// Annotations in the wrong order. The preferred order is: +// * declaration annotations +// * access modifiers such as `public` +// * type annotations +// * type + +class FormatAnnotationsAccessModifiers { + + @Nullable public Object myMethod1() { + return null; + } + + @Nullable + public Object myMethod2() { + return null; + } + + @Nullable + public + Object myMethod3() { + return null; + } + + @Nullable + @Deprecated + public Object myMethod4() { + return null; + } + + @Deprecated + @Nullable + public Object myMethod4a() { + return null; + } + + @Override + @Nullable + @Deprecated + public Object myMethod5() { + return null; + } + + @Nullable @Deprecated public Object myMethod6() { + return null; + } +} + +@Deprecated +@Interned +@MustCall("close") +@SuppressWarnings +public class MyClass3 { + // No body +} + +public +@Deprecated +@SuppressWarnings +@Interned +@MustCall("close") +class MyClass4 { + // No body +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersOutput.test new file mode 100644 index 0000000000..ffc6169c4c --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAccessModifiersOutput.test @@ -0,0 +1,54 @@ +// Annotations in the wrong order. The preferred order is: +// * declaration annotations +// * access modifiers such as `public` +// * type annotations +// * type + +class FormatAnnotationsAccessModifiers { + + @Nullable public Object myMethod1() { + return null; + } + + @Nullable public Object myMethod2() { + return null; + } + + @Nullable public + Object myMethod3() { + return null; + } + + @Nullable @Deprecated + public Object myMethod4() { + return null; + } + + @Deprecated + @Nullable public Object myMethod4a() { + return null; + } + + @Override + @Nullable @Deprecated + public Object myMethod5() { + return null; + } + + @Nullable @Deprecated public Object myMethod6() { + return null; + } +} + +@Deprecated +@Interned @MustCall("close") @SuppressWarnings +public class MyClass3 { + // No body +} + +public +@Deprecated +@SuppressWarnings +@Interned @MustCall("close") class MyClass4 { + // No body +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test new file mode 100644 index 0000000000..e822a8cc41 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test @@ -0,0 +1,11 @@ +class FormatAnnotationsAddRemove { + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized + String localized; +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test new file mode 100644 index 0000000000..355528bd88 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test @@ -0,0 +1,9 @@ +class FormatAnnotationsAddRemove { + + @Empty String e; + + @NonEmpty String ne; + + @Localized + String localized; +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test new file mode 100644 index 0000000000..a744b072a1 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test @@ -0,0 +1,31 @@ +class FormatAnnotationsInComments { + + // Here is a comment + @Interned + String m1() {} + + // Here is another comment + String m2() {} + + /** + * Here is a misformatted type annotation within a Javadoc comment. + * + * @Nullable + * String s; + */ + + @Nullable + @Interned + String m3(/* Don't get confused by other comments on the line with the type */) {} + + @Nullable + @Interned + String m3() {} // Still not confused + + /* + code snippets in regular comments do get re-formatted + + @Nullable + String s; + */ +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test new file mode 100644 index 0000000000..be65af24be --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test @@ -0,0 +1,25 @@ +class FormatAnnotationsInComments { + + // Here is a comment + @Interned String m1() {} + + // Here is another comment + String m2() {} + + /** + * Here is a misformatted type annotation within a Javadoc comment. + * + * @Nullable + * String s; + */ + + @Nullable @Interned String m3(/* Don't get confused by other comments on the line with the type */) {} + + @Nullable @Interned String m3() {} // Still not confused + + /* + code snippets in regular comments do get re-formatted + + @Nullable String s; + */ +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test new file mode 100644 index 0000000000..b93631db5a --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test @@ -0,0 +1,86 @@ +class FormatAnnotationsTest { + + public @Nullable + String s0 = null; + + @Deprecated + public @Nullable + String m0() {} + + @Nullable + String s1 = null; + + @Deprecated + @Nullable + String m1() {} + + @Nullable + @Deprecated + String m2() {} + + @Nullable + @Regex(2) + @Interned + String s2 = null; + + @Deprecated + @Nullable + @Regex(2) + @Interned + String m3() {} + + @Nullable + @Deprecated + @Regex(2) + @Interned + String m4() {} + + @Nullable + // a comment + @Regex(2) + @Interned + String s3 = null; + + @Nullable // a comment + @Regex(2) + @Interned + String s4 = null; + + @Nullable + @Regex(2) + @Interned + // a comment + String s5 = null; + + @Deprecated + // a comment + @Nullable + @Regex(2) + @Interned + String m5() {} + + @Deprecated + @Nullable + // a comment + @Regex(2) + @Interned + String m6() {} + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized + String localized; +} + +@Deprecated +@SuppressWarnings +public +@Interned +@MustCall("close") +class MyClass1 { + // No body +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test new file mode 100644 index 0000000000..6daabf198c --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test @@ -0,0 +1,58 @@ +class FormatAnnotationsTest { + + public @Nullable String s0 = null; + + @Deprecated + public @Nullable String m0() {} + + @Nullable String s1 = null; + + @Deprecated + @Nullable String m1() {} + + @Nullable @Deprecated + String m2() {} + + @Nullable @Regex(2) @Interned String s2 = null; + + @Deprecated + @Nullable @Regex(2) @Interned String m3() {} + + @Nullable @Deprecated + @Regex(2) @Interned String m4() {} + + @Nullable + // a comment + @Regex(2) @Interned String s3 = null; + + @Nullable // a comment + @Regex(2) @Interned String s4 = null; + + @Nullable @Regex(2) @Interned + // a comment + String s5 = null; + + @Deprecated + // a comment + @Nullable @Regex(2) @Interned String m5() {} + + @Deprecated + @Nullable + // a comment + @Regex(2) @Interned String m6() {} + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized String localized; +} + +@Deprecated +@SuppressWarnings +public +@Interned @MustCall("close") class MyClass1 { + // No body +} diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted.test index 7f7483f1bc..a0aeb21d0e 100644 --- a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted.test +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted.test @@ -1,8 +1,8 @@ - import mylib.UsedA; import mylib.UsedB; public class Java { + /** Some javadoc. */ public static void main(String[] args) { System.out.println( "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted18.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted18.test index 7a83a0a12c..a0aeb21d0e 100644 --- a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted18.test +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormatted18.test @@ -2,6 +2,7 @@ import mylib.UsedA; import mylib.UsedB; public class Java { + /** Some javadoc. */ public static void main(String[] args) { System.out.println( "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedAOSP.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedAOSP.test index dd1493b8c8..6b65d1f897 100644 --- a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedAOSP.test +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedAOSP.test @@ -1,8 +1,8 @@ - import mylib.UsedA; import mylib.UsedB; public class Java { + /** Some javadoc. */ public static void main(String[] args) { System.out.println( "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedReflowLongStrings.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedReflowLongStrings.test index 24e91a2ac8..8751e7e2d9 100644 --- a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedReflowLongStrings.test +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedReflowLongStrings.test @@ -2,6 +2,7 @@ import mylib.UsedA; import mylib.UsedB; public class Java { + /** Some javadoc. */ public static void main(String[] args) { System.out.println( "A very very very very very very very very very very very very very very very very very" diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedSkipJavadocFormatting.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedSkipJavadocFormatting.test new file mode 100644 index 0000000000..b3d1793729 --- /dev/null +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeFormattedSkipJavadocFormatting.test @@ -0,0 +1,14 @@ +import mylib.UsedA; +import mylib.UsedB; + +public class Java { + /** + * Some javadoc. + */ + public static void main(String[] args) { + System.out.println( + "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaCodeUnformatted.test b/testlib/src/main/resources/java/googlejavaformat/JavaCodeUnformatted.test index 18f2aca0e2..8e66776be8 100644 --- a/testlib/src/main/resources/java/googlejavaformat/JavaCodeUnformatted.test +++ b/testlib/src/main/resources/java/googlejavaformat/JavaCodeUnformatted.test @@ -4,6 +4,9 @@ import mylib.UsedB; import mylib.UsedA; public class Java { +/** + * Some javadoc. + */ public static void main(String[] args) { System.out.println("A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); UsedB.someMethod(); diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsDisabledFormatted.test b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsDisabledFormatted.test new file mode 100644 index 0000000000..5e62d24108 --- /dev/null +++ b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsDisabledFormatted.test @@ -0,0 +1,11 @@ +import java.nio.file.Paths; +import my.UsedB; +import org.xml.sax.InputSource; + +public class Java { + public static void main(String[] args) { + UsedB b = new UsedB(); + InputSource inputSource = new InputSource(); + Paths.get("dir"); + } +} diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsEnabledFormatted.test b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsEnabledFormatted.test new file mode 100644 index 0000000000..d0d4951601 --- /dev/null +++ b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsEnabledFormatted.test @@ -0,0 +1,13 @@ +import my.UsedB; + +import org.xml.sax.InputSource; + +import java.nio.file.Paths; + +public class Java { + public static void main(String[] args) { + UsedB b = new UsedB(); + InputSource inputSource = new InputSource(); + Paths.get("dir"); + } +} diff --git a/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsUnformatted.test b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsUnformatted.test new file mode 100644 index 0000000000..bfdbe83d88 --- /dev/null +++ b/testlib/src/main/resources/java/googlejavaformat/JavaWithReorderImportsUnformatted.test @@ -0,0 +1,11 @@ +import my.UsedB; +import java.nio.file.Paths; +import org.xml.sax.InputSource; + +public class Java { + public static void main(String[] args) { + UsedB b = new UsedB(); + InputSource inputSource = new InputSource(); + Paths.get("dir"); + } +} \ No newline at end of file diff --git a/testlib/src/main/resources/java/googlejavaformat/TextBlock.clean b/testlib/src/main/resources/java/googlejavaformat/TextBlock.clean index 8ee77fbf7a..bd558ffd96 100644 --- a/testlib/src/main/resources/java/googlejavaformat/TextBlock.clean +++ b/testlib/src/main/resources/java/googlejavaformat/TextBlock.clean @@ -3,7 +3,8 @@ import mylib.UsedB; public class Java { public static void main(String[] args) { - var a = """ + var a = + """ Howdy Partner! """; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test new file mode 100644 index 0000000000..b2346bb19c --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroups.test @@ -0,0 +1,16 @@ +import java.awt.*; +import java.lang.Runnable; +import java.lang.Thread; +import java.util.*; +import java.util.List; +import javax.annotation.Nullable; +import javax.inject.Inject; + +import org.dooda.Didoo; +import static com.foo.Bar; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; + +import static java.lang.Exception.*; +import static java.lang.Runnable.*; +import static org.hamcrest.Matchers.*; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroupsLeadingCatchAll.test b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroupsLeadingCatchAll.test new file mode 100644 index 0000000000..62efc65e21 --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeSortedImportsSubgroupsLeadingCatchAll.test @@ -0,0 +1,14 @@ +import static com.foo.Bar; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static java.lang.Exception.*; +import static java.lang.Runnable.*; +import static org.hamcrest.Matchers.*; +import java.awt.*; +import java.lang.Runnable; +import java.lang.Thread; +import java.util.*; +import java.util.List; +import javax.annotation.Nullable; +import javax.inject.Inject; +import org.dooda.Didoo; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeSortedLexicographic.test b/testlib/src/main/resources/java/importsorter/JavaCodeSortedLexicographic.test new file mode 100644 index 0000000000..2b59d52e58 --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeSortedLexicographic.test @@ -0,0 +1,17 @@ +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static com.example.Test.*; +import static com.example.Test.A_CONST; +import static com.example.Test.Nested.B_CONST; +import static com.example.Test.Z_CONST; + +import com.example.Test; +import com.example.Test.*; +import com.example.Test.Nested; +import com.example.a.A; +import com.example.b; +import com.example.c.C; +import com.sun.jna.platform.win32.COM.Unknown; +import com.sun.jna.platform.win32.Guid.CLSID; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeSortedSemanticSort.test b/testlib/src/main/resources/java/importsorter/JavaCodeSortedSemanticSort.test new file mode 100644 index 0000000000..3ab813b227 --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeSortedSemanticSort.test @@ -0,0 +1,17 @@ +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static com.example.Test.*; +import static com.example.Test.A_CONST; +import static com.example.Test.Z_CONST; +import static com.example.Test.Nested.B_CONST; + +import com.example.Test; +import com.example.Test.*; +import com.example.Test.Nested; +import com.example.b; +import com.example.a.A; +import com.example.c.C; +import com.sun.jna.platform.win32.Guid.CLSID; +import com.sun.jna.platform.win32.COM.Unknown; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test new file mode 100644 index 0000000000..35d8c465e4 --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedImportsSubgroups.test @@ -0,0 +1,15 @@ +import static java.lang.Exception.*; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import org.dooda.Didoo; +import java.util.List; +import javax.inject.Inject; +import java.lang.Thread; +import java.util.*; +import java.lang.Runnable; +import static org.hamcrest.Matchers.*; +import javax.annotation.Nullable; + +import static java.lang.Runnable.*; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.foo.Bar +import java.awt.*; diff --git a/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedSemanticSort.test b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedSemanticSort.test new file mode 100644 index 0000000000..7bfcfc6f7a --- /dev/null +++ b/testlib/src/main/resources/java/importsorter/JavaCodeUnsortedSemanticSort.test @@ -0,0 +1,19 @@ +import static com.example.Test.*; +import static com.example.Test.A_CONST; +import static com.example.Test.Z_CONST; +import static com.example.Test.Nested.B_CONST; + +import com.example.Test.Nested; +import com.example.Test; +import com.example.Test.*; + +import com.sun.jna.platform.win32.COM.Unknown; +import com.sun.jna.platform.win32.Guid.CLSID; + +import com.example.a.A; +import com.example.b; +import com.example.c.C; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.when; diff --git a/testlib/src/main/resources/java/palantirjavaformat/JavaCodeFormattedGoogle.test b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeFormattedGoogle.test new file mode 100644 index 0000000000..7a83a0a12c --- /dev/null +++ b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeFormattedGoogle.test @@ -0,0 +1,11 @@ +import mylib.UsedA; +import mylib.UsedB; + +public class Java { + public static void main(String[] args) { + System.out.println( + "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test new file mode 100644 index 0000000000..9cff5e17a1 --- /dev/null +++ b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test @@ -0,0 +1,39 @@ +import mylib.Unused; +import mylib.UsedA; +import mylib.UsedB; + +/** + * This is a test class with a long unformatted JavaDoc description. Lorem ipsum dolor sit amet, consectetur adipiscing + * elit. Vestibulum pulvinar condimentum elit, eget mollis magna sollicitudin in. Aenean pharetra nunc nec luctus + * consequat. Donec nec tincidunt quam, in auctor ipsum. Nam in sem orci. Maecenas interdum posuere orci a semper. Cras + * vulputate blandit metus, nec semper urna porttitor at. Praesent velit turpis, consequat in cursus eget, posuere eget + * magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque ante eros, sagittis sed tempus nec, + * rutrum ac arcu. Sed porttitor quam at enim commodo dictum. Sed fringilla tincidunt ex in aliquet. + * + * @author https://www.lipsum.com/ + * @since 0.0.2 + */ +public class Java { + /** + * A very simple method that I really like a lot? + * + *

Care for more details? + * + *

    + *
  • Too + *
  • bad + *
  • I + *
  • don't + *
  • have + *
  • any + *
+ * + * @param args Useless args, but see {@link Unused}, perhaps even {@link UsedA} or even {@link UsedB b }? + */ + public static void main(String[] args) { + System.out.println( + "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test new file mode 100644 index 0000000000..fcb3ad660c --- /dev/null +++ b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test @@ -0,0 +1,30 @@ + +import mylib.Unused; +import mylib.UsedB; +import mylib.UsedA; + +/** This is a test class with a long unformatted JavaDoc description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum pulvinar condimentum elit, eget mollis magna sollicitudin in. Aenean pharetra nunc nec luctus consequat. Donec nec tincidunt quam, in auctor ipsum. Nam in sem orci. Maecenas interdum posuere orci a semper. Cras vulputate blandit metus, nec semper urna porttitor at. Praesent velit turpis, consequat in cursus eget, posuere eget magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque ante eros, sagittis sed tempus nec, rutrum ac arcu. Sed porttitor quam at enim commodo dictum. Sed fringilla tincidunt ex in aliquet. + * @author https://www.lipsum.com/ + * @since 0.0.2 + */ +public class Java { + /** + * A very simple method that I + * really + * like + * a lot? + * + * Care for more details?
  • Too
  • bad
  • + *
  • I
  • don't
  • have
  • + *
  • any
  • + *
+ * + * @param args Useless args, but + * see {@link Unused}, perhaps even {@link UsedA} or even {@link UsedB b }? + */ +public static void main(String[] args) { +System.out.println("A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); +UsedB.someMethod(); +UsedA.someMethod(); +} +} \ No newline at end of file diff --git a/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithLicenseFormattedGoogle.test b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithLicenseFormattedGoogle.test new file mode 100644 index 0000000000..9ee9bcbad6 --- /dev/null +++ b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithLicenseFormattedGoogle.test @@ -0,0 +1,16 @@ +/* + * Some license stuff. + * Very official. + */ + +import mylib.UsedA; +import mylib.UsedB; + +public class Java { + public static void main(String[] args) { + System.out.println( + "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithPackageFormattedGoogle.test b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithPackageFormattedGoogle.test new file mode 100644 index 0000000000..4992ade147 --- /dev/null +++ b/testlib/src/main/resources/java/palantirjavaformat/JavaCodeWithPackageFormattedGoogle.test @@ -0,0 +1,13 @@ +package hello.world; + +import mylib.UsedA; +import mylib.UsedB; + +public class Java { + public static void main(String[] args) { + System.out.println( + "A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length."); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockFormatted.test b/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockFormatted.test new file mode 100644 index 0000000000..d6abc3685c --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockFormatted.test @@ -0,0 +1,28 @@ +package io.github.shafthq.shaft.tools.tms; + + + +import static io.restassured.RestAssured.*; + +public class XrayIntegrationHelper { + private static String getLinkJIRATicketRequestBody() { + return """ + { + "update":{ + "issuelinks":[ + { + "add":{ + "type":{ + "name":"Relates" + }, + "outwardIssue":{ + "key":"${TICKET_ID}" + } + } + } + ] + } + } + """; + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockUnformatted.test new file mode 100644 index 0000000000..1bfad32664 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/Jdk17TextBlockUnformatted.test @@ -0,0 +1,49 @@ +package io.github.shafthq.shaft.tools.tms; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.shaft.cli.FileActions; +import com.shaft.tools.io.ReportManager; +import io.github.shafthq.shaft.tools.io.helpers.ReportManagerHelper; +import io.restassured.config.RestAssuredConfig; +import io.restassured.config.SSLConfig; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Calendar; +import java.util.List; + +import static io.restassured.RestAssured.*; +import static io.restassured.config.EncoderConfig.encoderConfig; + + +public class XrayIntegrationHelper { + private static String getLinkJIRATicketRequestBody() { + return """ + { + "update":{ + "issuelinks":[ + { + "add":{ + "type":{ + "name":"Relates" + }, + "outwardIssue":{ + "key":"${TICKET_ID}" + } + } + } + ] + } + } + """; + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/RevelcFormatted.test b/testlib/src/main/resources/java/removeunusedimports/RevelcFormatted.test new file mode 100644 index 0000000000..672400fdb8 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/RevelcFormatted.test @@ -0,0 +1,81 @@ +/* + * 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. + */ + +@XmlSchema( + xmlns = { + @XmlNs(prefix = "order", namespaceURI = "http://www.camel.apache.org/jaxb/example/order/1"), + @XmlNs(prefix = "address", namespaceURI = "http://www.camel.apache.org/jaxb/example/address/1") + } +) +package net.revelc.code.imp; + +import com.foo.Type1; +import com.foo.Type2; +import com.foo.Type3; +import com.foo.Type4; +import com.foo.Type5; +import com.foo.Type6; +import com.foo.Type7; +import com.foo.Type8; +import com.foo.Type9; +import com.foo.Type10; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Component; + +import com.foo.Type11; +import com.foo.internal.Type12; +import com.foo.params.Type13; +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlSchema; + +import static org.junit.Assert.assertFalse; + +import static org.junit.Assert.*; + +/** + * The import of {@link HashMap} should not be stripped away, since it + * is used in this comment. + */ +// https://github.com/revelc/impsort-maven-plugin/blob/main/src/test/resources/UnusedImports.java +@Component +public class UnusedImports { + ImmutableMap immutable; + + /** + * The following should also not be removed: + * + * @param blah when {@link Type13} blah + * @see Map + */ + public List getList(String blah) { + assertFalse(false); + return null; + } + + /** + * {@link Type1#method()} + * {@link Type2#method(Type3, Type4)} + * {@link #method(Type5, Type6)} + * {@value Type7#field} + * @see Type8#method() + * @see Type9#method(Type10) + * @throws Type11 when {@link Type12} is seen + */ + public void foo() { + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/RevelcUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/RevelcUnformatted.test new file mode 100644 index 0000000000..bb43884a82 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/RevelcUnformatted.test @@ -0,0 +1,89 @@ +/* + * 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. + */ + +@XmlSchema( + xmlns = { + @XmlNs(prefix = "order", namespaceURI = "http://www.camel.apache.org/jaxb/example/order/1"), + @XmlNs(prefix = "address", namespaceURI = "http://www.camel.apache.org/jaxb/example/address/1") + } +) +package net.revelc.code.imp; + +import com.foo.Type1; +import com.foo.Type2; +import com.foo.Type3; +import com.foo.Type4; +import com.foo.Type5; +import com.foo.Type6; +import com.foo.Type7; +import com.foo.Type8; +import com.foo.Type9; +import com.foo.Type10; +import net.revelc.code.imp.Something; +import net.revelc.code.imp.Something.Else; +import net.revelc.code.imp.*; + +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import io.swagger.annotations.ApiOperation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.foo.Type11; +import com.foo.internal.Type12; +import com.foo.params.Type13; +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlSchema; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import static org.junit.Assert.*; + +/** + * The import of {@link HashMap} should not be stripped away, since it + * is used in this comment. + */ +// https://github.com/revelc/impsort-maven-plugin/blob/main/src/test/resources/UnusedImports.java +@Component +public class UnusedImports { + ImmutableMap immutable; + + /** + * The following should also not be removed: + * + * @param blah when {@link Type13} blah + * @see Map + */ + public List getList(String blah) { + assertFalse(false); + return null; + } + + /** + * {@link Type1#method()} + * {@link Type2#method(Type3, Type4)} + * {@link #method(Type5, Type6)} + * {@value Type7#field} + * @see Type8#method() + * @see Type9#method(Type10) + * @throws Type11 when {@link Type12} is seen + */ + public void foo() { + } +} diff --git a/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsFormatted.test b/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsFormatted.test new file mode 100644 index 0000000000..f0a0459883 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsFormatted.test @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package java.removeunusedimports; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +class SealedClassTests extends AbstractJupiterTestEngineTests { + + @Test + void sealedTestClassesAreTestClasses() { + executeTestsForClass(TestCase.class).testEvents() // + .assertStatistics(stats -> stats.finished(2).succeeded(1).failed(1)); + } + + sealed + abstract static class AbstractTestCase + permits TestCase + { + + @Test + void succeedingTest() { + assertTrue(true); + } + + @Test + void failingTest() { + fail("always fails"); + } + } + + static final class TestCase extends AbstractTestCase { + } + +} diff --git a/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsUnformatted.test b/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsUnformatted.test new file mode 100644 index 0000000000..942aa4d235 --- /dev/null +++ b/testlib/src/main/resources/java/removeunusedimports/SealedClassTestsUnformatted.test @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package java.removeunusedimports; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import useless.project.UnusedClass; + +class SealedClassTests extends AbstractJupiterTestEngineTests { + + @Test + void sealedTestClassesAreTestClasses() { + executeTestsForClass(TestCase.class).testEvents() // + .assertStatistics(stats -> stats.finished(2).succeeded(1).failed(1)); + } + + sealed + abstract static class AbstractTestCase + permits TestCase + { + + @Test + void succeedingTest() { + assertTrue(true); + } + + @Test + void failingTest() { + fail("always fails"); + } + } + + static final class TestCase extends AbstractTestCase { + } + +} diff --git a/testlib/src/main/resources/json/cucumberJsonSampleGsonAfter.json b/testlib/src/main/resources/json/cucumberJsonSampleGsonAfter.json new file mode 100644 index 0000000000..07e87bfd38 --- /dev/null +++ b/testlib/src/main/resources/json/cucumberJsonSampleGsonAfter.json @@ -0,0 +1,660 @@ +[ + { + "id": "account-holder-withdraws-cash", + "tags": [ + { + "name": "@featureTag", + "line": 1 + } + ], + "description": "This is description of the feature", + "name": "1st feature", + "keyword": "Feature", + "line": 2, + "elements": [ + { + "description": "Perfect background", + "name": "Activate Credit Card", + "keyword": "Background", + "line": 7, + "steps": [ + { + "result": { + "duration": 99107447000, + "status": "passed" + }, + "name": "I have a new credit card", + "keyword": "Given ", + "line": 8, + "match": { + "location": "ATMScenario.I_have_a_new_credit_card()" + }, + "embeddings": [ + { + "mime_type": "image/url", + "data": "" + }, + { + "data": "", + "media": { + "type": "text/plain" + } + } + ] + }, + { + "result": { + "duration": 9520000, + "status": "passed" + }, + "name": "My credit card is described as follow:", + "keyword": "And ", + "line": 9, + "match": { + "location": "ATMScenario.My_credit_card_is_described_as_follow" + }, + "doc_string": { + "content_type": "", + "line": 10, + "value": "{\n\"issuer\": {\n\"name\": \"Real Bank Inc.\",\n\"isn:\": \"RB55800093842N\"\n},\n\"card_number\": \"4896 0215 8478 6325\",\n\"holder\": \"A guy\"\n}" + } + }, + { + "result": { + "duration": 7040000, + "status": "passed" + }, + "name": "I confirm my pin number", + "keyword": "When ", + "line": 18, + "match": { + "location": "ATMScenario.I_confirm_my_pin_number()" + }, + "rows": [ + { + "cells": [ + "MÃŧller", + "Deutschland" + ], + "line": 2 + }, + { + "cells": [ + "NovÃĄkovÃĄ", + "Česko" + ], + "line": 3 + }, + { + "cells": [ + "Kovačević", + "Hrvatska" + ], + "line": 4 + }, + { + "cells": [ + "Î ÎąĪ€ÎąÎ´ĪŒĪ€ÎŋĪ…ÎģÎŋĪ‚", + "Î ÎąĪ€ÎąÎ´ĪŒĪ€ÎŋĪ…ÎģÎŋĪ‚" + ], + "line": 5 + }, + { + "cells": [ + "įŊ—/įž…", + "中國" + ], + "line": 6 + } + ] + }, + { + "result": { + "duration": 111111, + "status": "passed" + }, + "name": "the card should be activated", + "keyword": "Then ", + "line": 19, + "match": { + "location": "ATMScenario.the_card_should_be_activated()" + } + } + ], + "type": "background" + }, + { + "id": "account-holder-withdraws-cash;account-has-'sufficient-funds';;2", + "tags": [ + { + "name": "@fast", + "line": 21 + }, + { + "name": "@featureTag", + "line": 1 + }, + { + "name": "@checkout", + "line": 21 + } + ], + "description": "Account holder withdraws cash", + "name": "Account has ", + "keyword": "Scenario Outline", + "line": 33, + "steps": [ + { + "result": { + "duration": 17007000, + "status": "passed" + }, + "name": "the account balance is 100", + "keyword": "Given ", + "line": 23, + "match": { + "arguments": [ + { + "val": "100", + "offset": 23 + } + ], + "location": "ATMScenario.createAccount(int)" + } + }, + { + "result": { + "duration": 33444444, + "status": "passed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 24, + "match": { + "arguments": [ + { + "val": "", + "offset": 0 + } + ], + "location": "ATMScenario.createCreditCard()" + } + }, + { + "result": { + "duration": 44333333, + "status": "passed" + }, + "name": "100 is contained in the machine", + "keyword": "And ", + "line": 25, + "match": { + "arguments": [ + { + "val": "100", + "offset": 0 + } + ], + "location": "ATMScenario.createATM(int)" + }, + "matchedColumns": [ + 1 + ] + }, + { + "result": { + "duration": 11000001, + "status": "passed" + }, + "name": "the Account Holder requests 10, entering PIN 1234", + "keyword": "When ", + "line": 26, + "match": { + "arguments": [ + { + "val": "10", + "offset": 28 + }, + { + "val": "1234", + "offset": 45 + } + ], + "location": "ATMScenario.requestMoney(int)" + }, + "matchedColumns": [ + 2 + ] + }, + { + "result": { + "duration": 3220000, + "status": "passed" + }, + "name": "the ATM should dispense 10 monetary units", + "keyword": "Then ", + "line": 27, + "match": { + "arguments": [ + { + "val": "10", + "offset": 24 + }, + { + "val": "", + "offset": 0 + } + ], + "location": "ATMScenario.checkMoney(int)" + }, + "matchedColumns": [ + 3 + ] + }, + { + "result": { + "duration": 30000000, + "status": "passed" + }, + "name": "the account balance should be 90", + "keyword": "And ", + "line": 28, + "arguments": [ + { + "rows": [ + { + "cells": [ + "max", + "min" + ] + }, + { + "cells": [ + "20", + "3" + ] + } + ] + } + ], + "match": { + "location": "ATMScenario.checkBalance(int)" + }, + "matchedColumns": [ + 2 + ] + } + ], + "type": "scenario", + "after": [ + { + "result": { + "duration": 60744700, + "status": "passed", + "error_message": "Completed" + }, + "match": { + "location": "MachineFactory.timeout()" + } + } + ] + } + ], + "uri": "net/masterthought/example(s)/ATM:æąäēŦ.feature" + }, + { + "id": "account-holder-withdraws-more-cash", + "description": "As an Account Holder\nI want to withdraw cash from an ATM,
so that I can get money when the bank is closed", + "name": "Second feature", + "keyword": "Feature", + "line": 1, + "elements": [ + { + "id": "account-holder-withdraws-more-cash;account-has-sufficient-funds;;2", + "tags": [ + { + "name": "@checkout", + "line": 101 + } + ], + "before": [ + { + "output": [ + "System version: beta3" + ], + "result": { + "duration": 10744700, + "status": "passed" + }, + "match": { + "location": "MachineFactory.findCashMachine()" + } + }, + { + "result": { + "duration": 1000001, + "status": "failed", + "error_message": " \n" + }, + "match": { + "location": "MachineFactory.wait()" + } + } + ], + "description": "Account holder withdraws more cash", + "name": "Account may not have sufficient funds", + "keyword": "Scenario Outline", + "line": 19, + "steps": [ + { + "result": { + "status": "undefined" + }, + "name": "the account balance is 100", + "keyword": "Given ", + "line": 7, + "match": { + "arguments": [ + { + "val": "100", + "offset": 23 + }, + {} + ] + }, + "matchedColumns": [ + 0 + ], + "before": [ + { + "embeddings": [ + { + "mime_type": "text/plain", + "data": "" + } + ], + "result": { + "duration": 410802047, + "status": "failed" + } + } + ] + }, + { + "result": { + "duration": 13000, + "status": "passed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 8, + "match": { + "arguments": [ + { + "val": "", + "offset": 17 + } + ], + "location": "ATMScenario.createCreditCard()" + }, + "after": [ + { + "result": { + "duration": 410802048, + "status": "passed" + }, + "match": { + "location": "StepHook.afterStep()" + } + } + ] + }, + { + "result": { + "duration": 36000, + "status": "passed" + }, + "name": "the machine contains 100", + "keyword": "And ", + "line": 9, + "match": { + "arguments": [ + { + "val": "100", + "offset": 21 + } + ], + "location": "ATMScenario.createATM(int)" + }, + "matchedColumns": [ + 1 + ] + }, + { + "result": { + "duration": 32000, + "status": "passed" + }, + "name": "the Account Holder requests 20", + "keyword": "When ", + "line": 10, + "match": { + "arguments": [ + { + "val": "20", + "offset": 28 + } + ], + "location": "ATMScenario.requestMoney(int)" + }, + "matchedColumns": [ + 2 + ] + }, + { + "result": { + "duration": 36000, + "status": "passed" + }, + "name": "the ATM should dispense 20", + "keyword": "Then ", + "line": 11, + "match": { + "arguments": [ + { + "val": "20", + "offset": 24 + } + ], + "location": "ATMScenario.checkMoney(int)" + }, + "matchedColumns": [ + 3 + ] + }, + { + "result": { + "duration": 1933000, + "status": "skipped", + "error_message": "java.lang.AssertionError: \nExpected: is <80>\n got: <90>\n\n\tat org.junit.Assert.assertThat(Assert.java:780)\n\tat org.junit.Assert.assertThat(Assert.java:738)\n\tat net.masterthought.example.ATMScenario.checkBalance(ATMScenario.java:69)\n\tat âœŊ.And the account balance should be 90(net/masterthought/example/ATMK.feature:12)\n" + }, + "name": "the account balance should be 90", + "keyword": "And ", + "line": 12, + "match": { + "arguments": [ + { + "val": "90", + "offset": 30 + } + ], + "location": "ATMScenario.checkBalance(int)" + }, + "matchedColumns": [ + 4 + ], + "embeddings": [ + { + "mime_type": "image/png", + "data": "", + "name": "Some PNG image" + }, + { + "mime_type": "image/jpeg", + "data": "" + }, + { + "mime_type": "text/plain", + "data": "" + }, + { + "mime_type": "text/html", + "data": "", + "name": "Some HTML embedding" + }, + { + "mime_type": "text/xml", + "data": "" + }, + { + "mime_type": "image/svg+xml", + "data": "" + }, + { + "mime_type": "js", + "data": "" + }, + { + "mime_type": "text/plain", + "data": "" + }, + { + "mime_type": "text/csv", + "data": "" + }, + { + "mime_type": "video/mp4", + "data": "" + } + ] + }, + { + "result": { + "status": "pending" + }, + "name": "the card should be returned", + "keyword": "And ", + "line": 13, + "match": { + "location": "ATMScenario.cardShouldBeReturned()" + }, + "embeddings": [ + { + "mime_type": "application/json", + "data": "" + } + ] + }, + { + "result": { + "status": "skipped" + }, + "name": "its not implemented", + "keyword": "And ", + "line": 14, + "match": { + "location": "ATMScenario.its_not_implemented()" + }, + "output": [ + [ + "Could not connect to the server @Rocky@" + ], + [ + "Could not connect to the server @Mike@" + ] + ] + }, + { + "result": { + "status": "failed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 15, + "match": { + "location": "ATMScenario.createCreditCard()" + }, + "output": [ + "Checkpoints", + 232 + ] + }, + { + "result": { + "duration": 90000000, + "status": "ambiguous" + }, + "name": "the card should be returned", + "keyword": "And ", + "line": 29, + "match": { + "location": "ATMScenario.cardShouldBeReturned()" + } + } + ], + "type": "scenario", + "after": [ + { + "result": { + "duration": 64700000, + "status": "undefined", + "error_message": "Undefined step" + }, + "match": { + "location": "any.error()" + }, + "embeddings": [ + { + "mime_type": "image/png", + "data": "" + } + ] + } + ] + }, + { + "id": "account-holder-withdraws-more-cash;clean-up", + "name": "Clean-up", + "keyword": "Scenario", + "line": 31, + "steps": [ + { + "result": { + "duration": 560000, + "status": "passed" + }, + "name": "Stream closing", + "keyword": "Given ", + "line": 32 + } + ], + "type": "scenario" + }, + { + "id": "undefined-result", + "name": "This step has no result...", + "keyword": "Scenario", + "line": 35, + "steps": [ + { + "name": " - even it should", + "keyword": "Given ", + "line": 36 + } + ], + "type": "scenario" + } + ], + "uri": "net/masterthought/example/ATMK.feature" + } +] diff --git a/testlib/src/main/resources/json/cucumberJsonSampleGsonBefore.json b/testlib/src/main/resources/json/cucumberJsonSampleGsonBefore.json new file mode 100644 index 0000000000..8250630cf7 --- /dev/null +++ b/testlib/src/main/resources/json/cucumberJsonSampleGsonBefore.json @@ -0,0 +1,660 @@ +[ + { + "id": "account-holder-withdraws-cash", + "tags": [ + { + "name": "@featureTag", + "line": 1 + } + ], + "description": "This is description of the feature", + "name": "1st feature", + "keyword": "Feature", + "line": 2, + "elements": [ + { + "description": "Perfect background", + "name": "Activate Credit Card", + "keyword": "Background", + "line": 7, + "steps": [ + { + "result": { + "duration": 99107447000, + "status": "passed" + }, + "name": "I have a new credit card", + "keyword": "Given ", + "line": 8, + "match": { + "location": "ATMScenario.I_have_a_new_credit_card()" + }, + "embeddings": [ + { + "mime_type": "image/url", + "data": "" + }, + { + "data": "", + "media": { + "type": "text/plain" + } + } + ] + }, + { + "result": { + "duration": 9520000, + "status": "passed" + }, + "name": "My credit card is described as follow:", + "keyword": "And ", + "line": 9, + "match": { + "location": "ATMScenario.My_credit_card_is_described_as_follow" + }, + "doc_string": { + "content_type": "", + "line": 10, + "value": "{\n\"issuer\": {\n\"name\": \"Real Bank Inc.\",\n\"isn:\": \"RB55800093842N\"\n},\n\"card_number\": \"4896 0215 8478 6325\",\n\"holder\": \"A guy\"\n}" + } + }, + { + "result": { + "duration": 7040000, + "status": "passed" + }, + "name": "I confirm my pin number", + "keyword": "When ", + "line": 18, + "match": { + "location": "ATMScenario.I_confirm_my_pin_number()" + }, + "rows": [ + { + "cells": [ + "MÃŧller", + "Deutschland" + ], + "line": 2 + }, + { + "cells": [ + "NovÃĄkovÃĄ", + "Česko" + ], + "line": 3 + }, + { + "cells": [ + "Kovačević", + "Hrvatska" + ], + "line": 4 + }, + { + "cells": [ + "Î ÎąĪ€ÎąÎ´ĪŒĪ€ÎŋĪ…ÎģÎŋĪ‚", + "Î ÎąĪ€ÎąÎ´ĪŒĪ€ÎŋĪ…ÎģÎŋĪ‚" + ], + "line": 5 + }, + { + "cells": [ + "įŊ—/įž…", + "中國" + ], + "line": 6 + } + ] + }, + { + "result": { + "duration": 111111, + "status": "passed" + }, + "name": "the card should be activated", + "keyword": "Then ", + "line": 19, + "match": { + "location": "ATMScenario.the_card_should_be_activated()" + } + } + ], + "type": "background" + }, + { + "id": "account-holder-withdraws-cash;account-has-\u0027sufficient-funds\u0027;;2", + "tags": [ + { + "name": "@fast", + "line": 21 + }, + { + "name": "@featureTag", + "line": 1 + }, + { + "name": "@checkout", + "line": 21 + } + ], + "description": "Account holder withdraws cash", + "name": "Account has ", + "keyword": "Scenario Outline", + "line": 33, + "steps": [ + { + "result": { + "duration": 17007000, + "status": "passed" + }, + "name": "the account balance is 100", + "keyword": "Given ", + "line": 23, + "match": { + "arguments": [ + { + "val": "100", + "offset": 23 + } + ], + "location": "ATMScenario.createAccount(int)" + } + }, + { + "result": { + "duration": 33444444, + "status": "passed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 24, + "match": { + "arguments": [ + { + "val": "", + "offset": 0 + } + ], + "location": "ATMScenario.createCreditCard()" + } + }, + { + "result": { + "duration": 44333333, + "status": "passed" + }, + "name": "100 is contained in the machine", + "keyword": "And ", + "line": 25, + "match": { + "arguments": [ + { + "val": "100", + "offset": 0 + } + ], + "location": "ATMScenario.createATM(int)" + }, + "matchedColumns": [ + 1 + ] + }, + { + "result": { + "duration": 11000001, + "status": "passed" + }, + "name": "the Account Holder requests 10, entering PIN 1234", + "keyword": "When ", + "line": 26, + "match": { + "arguments": [ + { + "val": "10", + "offset": 28 + }, + { + "val": "1234", + "offset": 45 + } + ], + "location": "ATMScenario.requestMoney(int)" + }, + "matchedColumns": [ + 2 + ] + }, + { + "result": { + "duration": 3220000, + "status": "passed" + }, + "name": "the ATM should dispense 10 monetary units", + "keyword": "Then ", + "line": 27, + "match": { + "arguments": [ + { + "val": "10", + "offset": 24 + }, + { + "val": "", + "offset": 0 + } + ], + "location": "ATMScenario.checkMoney(int)" + }, + "matchedColumns": [ + 3 + ] + }, + { + "result": { + "duration": 30000000, + "status": "passed" + }, + "name": "the account balance should be 90", + "keyword": "And ", + "line": 28, + "arguments": [ + { + "rows": [ + { + "cells": [ + "max", + "min" + ] + }, + { + "cells": [ + "20", + "3" + ] + } + ] + } + ], + "match": { + "location": "ATMScenario.checkBalance(int)" + }, + "matchedColumns": [ + 2 + ] + } + ], + "type": "scenario", + "after": [ + { + "result": { + "duration": 60744700, + "status": "passed", + "error_message": "Completed" + }, + "match": { + "location": "MachineFactory.timeout()" + } + } + ] + } + ], + "uri": "net/masterthought/example(s)/ATM:æąäēŦ.feature" + }, + { + "id": "account-holder-withdraws-more-cash", + "description": "As an Account Holder\nI want to withdraw cash from an ATM,
so that I can get money when the bank is closed", + "name": "Second feature", + "keyword": "Feature", + "line": 1, + "elements": [ + { + "id": "account-holder-withdraws-more-cash;account-has-sufficient-funds;;2", + "tags": [ + { + "name": "@checkout", + "line": 101 + } + ], + "before": [ + { + "output": [ + "System version: beta3" + ], + "result": { + "duration": 10744700, + "status": "passed" + }, + "match": { + "location": "MachineFactory.findCashMachine()" + } + }, + { + "result": { + "duration": 1000001, + "status": "failed", + "error_message": " \n" + }, + "match": { + "location": "MachineFactory.wait()" + } + } + ], + "description": "Account holder withdraws more cash", + "name": "Account may not have sufficient funds", + "keyword": "Scenario Outline", + "line": 19, + "steps": [ + { + "result": { + "status": "undefined" + }, + "name": "the account balance is 100", + "keyword": "Given ", + "line": 7, + "match": { + "arguments": [ + { + "val": "100", + "offset": 23 + }, + {} + ] + }, + "matchedColumns": [ + 0 + ], + "before": [ + { + "embeddings": [ + { + "mime_type": "text/plain", + "data": "" + } + ], + "result": { + "duration": 410802047, + "status": "failed" + } + } + ] + }, + { + "result": { + "duration": 13000, + "status": "passed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 8, + "match": { + "arguments": [ + { + "val": "", + "offset": 17 + } + ], + "location": "ATMScenario.createCreditCard()" + }, + "after": [ + { + "result": { + "duration": 410802048, + "status": "passed" + }, + "match": { + "location": "StepHook.afterStep()" + } + } + ] + }, + { + "result": { + "duration": 36000, + "status": "passed" + }, + "name": "the machine contains 100", + "keyword": "And ", + "line": 9, + "match": { + "arguments": [ + { + "val": "100", + "offset": 21 + } + ], + "location": "ATMScenario.createATM(int)" + }, + "matchedColumns": [ + 1 + ] + }, + { + "result": { + "duration": 32000, + "status": "passed" + }, + "name": "the Account Holder requests 20", + "keyword": "When ", + "line": 10, + "match": { + "arguments": [ + { + "val": "20", + "offset": 28 + } + ], + "location": "ATMScenario.requestMoney(int)" + }, + "matchedColumns": [ + 2 + ] + }, + { + "result": { + "duration": 36000, + "status": "passed" + }, + "name": "the ATM should dispense 20", + "keyword": "Then ", + "line": 11, + "match": { + "arguments": [ + { + "val": "20", + "offset": 24 + } + ], + "location": "ATMScenario.checkMoney(int)" + }, + "matchedColumns": [ + 3 + ] + }, + { + "result": { + "duration": 1933000, + "status": "skipped", + "error_message": "java.lang.AssertionError: \nExpected: is \u003c80\u003e\n got: \u003c90\u003e\n\n\tat org.junit.Assert.assertThat(Assert.java:780)\n\tat org.junit.Assert.assertThat(Assert.java:738)\n\tat net.masterthought.example.ATMScenario.checkBalance(ATMScenario.java:69)\n\tat âœŊ.And the account balance should be 90(net/masterthought/example/ATMK.feature:12)\n" + }, + "name": "the account balance should be 90", + "keyword": "And ", + "line": 12, + "match": { + "arguments": [ + { + "val": "90", + "offset": 30 + } + ], + "location": "ATMScenario.checkBalance(int)" + }, + "matchedColumns": [ + 4 + ], + "embeddings": [ + { + "mime_type": "image/png", + "data": "", + "name": "Some PNG image" + }, + { + "mime_type": "image/jpeg", + "data": "" + }, + { + "mime_type": "text/plain", + "data": "" + }, + { + "mime_type": "text/html", + "data": "", + "name": "Some HTML embedding" + }, + { + "mime_type": "text/xml", + "data": "" + }, + { + "mime_type": "image/svg+xml", + "data": "" + }, + { + "mime_type": "js", + "data": "" + }, + { + "mime_type": "text/plain", + "data": "" + }, + { + "mime_type": "text/csv", + "data": "" + }, + { + "mime_type": "video/mp4", + "data": "" + } + ] + }, + { + "result": { + "status": "pending" + }, + "name": "the card should be returned", + "keyword": "And ", + "line": 13, + "match": { + "location": "ATMScenario.cardShouldBeReturned()" + }, + "embeddings": [ + { + "mime_type": "application/json", + "data": "" + } + ] + }, + { + "result": { + "status": "skipped" + }, + "name": "its not implemented", + "keyword": "And ", + "line": 14, + "match": { + "location": "ATMScenario.its_not_implemented()" + }, + "output": [ + [ + "Could not connect to the server @Rocky@" + ], + [ + "Could not connect to the server @Mike@" + ] + ] + }, + { + "result": { + "status": "failed" + }, + "name": "the card is valid", + "keyword": "And ", + "line": 15, + "match": { + "location": "ATMScenario.createCreditCard()" + }, + "output": [ + "Checkpoints", + 232 + ] + }, + { + "result": { + "duration": 90000000, + "status": "ambiguous" + }, + "name": "the card should be returned", + "keyword": "And ", + "line": 29, + "match": { + "location": "ATMScenario.cardShouldBeReturned()" + } + } + ], + "type": "scenario", + "after": [ + { + "result": { + "duration": 64700000, + "status": "undefined", + "error_message": "Undefined step" + }, + "match": { + "location": "any.error()" + }, + "embeddings": [ + { + "mime_type": "image/png", + "data": "" + } + ] + } + ] + }, + { + "id": "account-holder-withdraws-more-cash;clean-up", + "name": "Clean-up", + "keyword": "Scenario", + "line": 31, + "steps": [ + { + "result": { + "duration": 560000, + "status": "passed" + }, + "name": "Stream closing", + "keyword": "Given ", + "line": 32 + } + ], + "type": "scenario" + }, + { + "id": "undefined-result", + "name": "This step has no result...", + "keyword": "Scenario", + "line": 35, + "steps": [ + { + "name": " - even it should", + "keyword": "Given ", + "line": 36 + } + ], + "type": "scenario" + } + ], + "uri": "net/masterthought/example/ATMK.feature" + } +] diff --git a/testlib/src/main/resources/json/escapeHtmlGsonAfter.json b/testlib/src/main/resources/json/escapeHtmlGsonAfter.json new file mode 100644 index 0000000000..f83692da2a --- /dev/null +++ b/testlib/src/main/resources/json/escapeHtmlGsonAfter.json @@ -0,0 +1,4 @@ +{ + "escapedHtml": "\u003ca href\u003d\u0027https://www.google.com/search?q\u003ddiffplug-spotless\u0026tbm\u003disch\u0027\u003eSee some pictures!\u003c\\a\u003e", + "rawHtml": "\u003ca href\u003d\u0027https://www.google.com/search?q\u003ddiffplug-spotless\u0026tbm\u003disch\u0027\u003eSee some pictures!\u003c\\a\u003e" +} diff --git a/testlib/src/main/resources/json/escapeHtmlGsonAfterDisabled.json b/testlib/src/main/resources/json/escapeHtmlGsonAfterDisabled.json new file mode 100644 index 0000000000..fe033341f8 --- /dev/null +++ b/testlib/src/main/resources/json/escapeHtmlGsonAfterDisabled.json @@ -0,0 +1,4 @@ +{ + "escapedHtml": "See some pictures!<\\a>", + "rawHtml": "See some pictures!<\\a>" +} diff --git a/testlib/src/main/resources/json/escapeHtmlGsonBefore.json b/testlib/src/main/resources/json/escapeHtmlGsonBefore.json new file mode 100644 index 0000000000..400e862d99 --- /dev/null +++ b/testlib/src/main/resources/json/escapeHtmlGsonBefore.json @@ -0,0 +1,4 @@ +{ + "escapedHtml": "\u003ca href\u003d\u0027https://www.google.com/search?q\u003ddiffplug-spotless\u0026tbm\u003disch\u0027\u003eSee some pictures!\u003c\\a\u003e", + "rawHtml": "See some pictures!<\\a>" +} diff --git a/testlib/src/main/resources/json/objectWithNullGsonAfter.json b/testlib/src/main/resources/json/objectWithNullGsonAfter.json new file mode 100644 index 0000000000..368632dd8a --- /dev/null +++ b/testlib/src/main/resources/json/objectWithNullGsonAfter.json @@ -0,0 +1,4 @@ +{ + "value": null, + "another": 1 +} diff --git a/testlib/src/main/resources/json/objectWithNullGsonBefore.json b/testlib/src/main/resources/json/objectWithNullGsonBefore.json new file mode 100644 index 0000000000..f68567c97c --- /dev/null +++ b/testlib/src/main/resources/json/objectWithNullGsonBefore.json @@ -0,0 +1,4 @@ +{ +"value": null, +"another": 1 +} diff --git a/testlib/src/main/resources/json/patchObjectAfterReplaceString.json b/testlib/src/main/resources/json/patchObjectAfterReplaceString.json new file mode 100644 index 0000000000..1da41d5ac2 --- /dev/null +++ b/testlib/src/main/resources/json/patchObjectAfterReplaceString.json @@ -0,0 +1,11 @@ +{ + "abc": "ghi", + "obj": { + "arr": [ + 1, + 2, + 3 + ], + "val": 5 + } +} diff --git a/testlib/src/main/resources/json/patchObjectAfterReplaceWithObject.json b/testlib/src/main/resources/json/patchObjectAfterReplaceWithObject.json new file mode 100644 index 0000000000..533691261c --- /dev/null +++ b/testlib/src/main/resources/json/patchObjectAfterReplaceWithObject.json @@ -0,0 +1,13 @@ +{ + "abc": { + "def": "ghi" + }, + "obj": { + "arr": [ + 1, + 2, + 3 + ], + "val": 5 + } +} diff --git a/testlib/src/main/resources/json/patchObjectBefore.json b/testlib/src/main/resources/json/patchObjectBefore.json new file mode 100644 index 0000000000..070dfc481e --- /dev/null +++ b/testlib/src/main/resources/json/patchObjectBefore.json @@ -0,0 +1,11 @@ +{ + "abc": "def", + "obj": { + "arr": [ + 1, + 2, + 3 + ], + "val": 5 + } +} diff --git a/testlib/src/main/resources/json/singletonArrayAfter_Jackson.json b/testlib/src/main/resources/json/singletonArrayAfter_Jackson.json new file mode 100644 index 0000000000..243bc2550b --- /dev/null +++ b/testlib/src/main/resources/json/singletonArrayAfter_Jackson.json @@ -0,0 +1 @@ +[ 1, 2, 3, 4 ] \ No newline at end of file diff --git a/testlib/src/main/resources/json/singletonArrayBefore.json b/testlib/src/main/resources/json/singletonArrayBefore.json index 8290d39198..18d09f95fe 100644 --- a/testlib/src/main/resources/json/singletonArrayBefore.json +++ b/testlib/src/main/resources/json/singletonArrayBefore.json @@ -1 +1 @@ -[ 1, 2, 3, 4 ] +[ 1 , 2, 3, 4 ] diff --git a/testlib/src/main/resources/json/sortByKeysAfter.json b/testlib/src/main/resources/json/sortByKeysAfter.json new file mode 100644 index 0000000000..070904e872 --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfter.json @@ -0,0 +1,25 @@ +{ + "A": 1, + "X": 2, + "_arraysNotSorted": [ + 3, + 2, + 1 + ], + "_objectsInArraysAreSorted": [ + { + "a": 1, + "b": 2 + } + ], + "a": 3, + "c": 4, + "x": 5, + "z": { + "A": 1, + "X": 2, + "a": 3, + "c": 4, + "x": 5 + } +} diff --git a/testlib/src/main/resources/json/sortByKeysAfterDisabled.json b/testlib/src/main/resources/json/sortByKeysAfterDisabled.json new file mode 100644 index 0000000000..eb9e38241f --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfterDisabled.json @@ -0,0 +1,25 @@ +{ + "z": { + "c": 4, + "x": 5, + "X": 2, + "A": 1, + "a": 3 + }, + "c": 4, + "x": 5, + "X": 2, + "A": 1, + "a": 3, + "_arraysNotSorted": [ + 3, + 2, + 1 + ], + "_objectsInArraysAreSorted": [ + { + "b": 2, + "a": 1 + } + ] +} diff --git a/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json b/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json new file mode 100644 index 0000000000..cd7ebc2be1 --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfterDisabled_Simple.json @@ -0,0 +1,23 @@ +{ + "A": 1, + "a": 3, + "c": 4, + "x": 5, + "X": 2, + "z": { + "A": 1, + "a": 3, + "c": 4, + "x": 5, + "X": 2 + }, + "_objectsInArraysAreSorted": [{ + "a": 1, + "b": 2 + }], + "_arraysNotSorted": [ + 3, + 2, + 1 + ] +} diff --git a/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json b/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json new file mode 100644 index 0000000000..25ce5dd094 --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json @@ -0,0 +1,19 @@ +{ + "A" : 1, + "X" : 2, + "_arraysNotSorted" : [ 3, 2, 1 ], + "_objectsInArraysAreSorted" : [ { + "a" : 1, + "b" : 2 + } ], + "a" : 3, + "c" : 4, + "x" : 5, + "z" : { + "A" : 1, + "X" : 2, + "a" : 3, + "c" : 4, + "x" : 5 + } +} \ No newline at end of file diff --git a/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json b/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json new file mode 100644 index 0000000000..25ce5dd094 --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json @@ -0,0 +1,19 @@ +{ + "A" : 1, + "X" : 2, + "_arraysNotSorted" : [ 3, 2, 1 ], + "_objectsInArraysAreSorted" : [ { + "a" : 1, + "b" : 2 + } ], + "a" : 3, + "c" : 4, + "x" : 5, + "z" : { + "A" : 1, + "X" : 2, + "a" : 3, + "c" : 4, + "x" : 5 + } +} \ No newline at end of file diff --git a/testlib/src/main/resources/json/sortByKeysBefore.json b/testlib/src/main/resources/json/sortByKeysBefore.json new file mode 100644 index 0000000000..eb9e38241f --- /dev/null +++ b/testlib/src/main/resources/json/sortByKeysBefore.json @@ -0,0 +1,25 @@ +{ + "z": { + "c": 4, + "x": 5, + "X": 2, + "A": 1, + "a": 3 + }, + "c": 4, + "x": 5, + "X": 2, + "A": 1, + "a": 3, + "_arraysNotSorted": [ + 3, + 2, + 1 + ], + "_objectsInArraysAreSorted": [ + { + "b": 2, + "a": 1 + } + ] +} diff --git a/testlib/src/main/resources/kotlin/diktat/main.clean b/testlib/src/main/resources/kotlin/diktat/main.clean index 4405165ed5..c46b4542b0 100644 --- a/testlib/src/main/resources/kotlin/diktat/main.clean +++ b/testlib/src/main/resources/kotlin/diktat/main.clean @@ -1,7 +1,7 @@ package org.cqfn.diktat.example.gradle.multiproject /** - * @return print. + * @return something */ class Main { /** @@ -9,7 +9,7 @@ class Main { * */ fun foo() { - println(";") - println() + bar(";") + bar() } } diff --git a/testlib/src/main/resources/kotlin/diktat/main.dirty b/testlib/src/main/resources/kotlin/diktat/main.dirty index 5f7be993aa..a5c225cc4f 100644 --- a/testlib/src/main/resources/kotlin/diktat/main.dirty +++ b/testlib/src/main/resources/kotlin/diktat/main.dirty @@ -1,6 +1,6 @@ package org.cqfn.diktat.example.gradle.multiproject /** - * @return print. + * @return something */ class Main { /** @@ -8,7 +8,7 @@ class Main { * */ fun foo() { - println(";") - println(); + bar(";") + bar(); } } diff --git a/testlib/src/main/resources/kotlin/ktfmt/basic-dropbox-style.clean b/testlib/src/main/resources/kotlin/ktfmt/basic-dropbox-style.clean new file mode 100644 index 0000000000..428b6d69fb --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/basic-dropbox-style.clean @@ -0,0 +1,14 @@ +import a.* +import a.b +import a.b.c.* +import kotlinx.android.synthetic.main.layout_name.* + +fun main() { + fun name() { + a() + return b + } + println( + ";") + println() +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/continuation.clean b/testlib/src/main/resources/kotlin/ktfmt/continuation.clean new file mode 100644 index 0000000000..11677928fc --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/continuation.clean @@ -0,0 +1,6 @@ +fun myFunction() { + val location = + restTemplate.postForLocation( + "/v1/my-api", mapOf("name" to "some-name", "url" to "https://www.google.com")) + return location +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/continuation.dirty b/testlib/src/main/resources/kotlin/ktfmt/continuation.dirty new file mode 100644 index 0000000000..3652274d12 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/continuation.dirty @@ -0,0 +1,4 @@ +fun myFunction() { + val location = restTemplate.postForLocation("/v1/my-api", mapOf("name" to "some-name", "url" to "https://www.google.com")) + return location +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean b/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean new file mode 100644 index 0000000000..0fce31c7ec --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean @@ -0,0 +1,12 @@ +import a.* +import a.b +import a.b.c.* +import kotlinx.android.synthetic.main.layout_name.* + +fun main() {} + +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a() + functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width.clean b/testlib/src/main/resources/kotlin/ktfmt/max-width.clean new file mode 100644 index 0000000000..4592181044 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width.clean @@ -0,0 +1,12 @@ +import a.* +import a.b +import a.b.c.* +import kotlinx.android.synthetic.main.layout_name.* + +fun main() {} + +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a() + functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty b/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty new file mode 100644 index 0000000000..ef9504e1c3 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty @@ -0,0 +1,12 @@ +import a.* + +import kotlinx.android.synthetic.main.layout_name.* + +import a.b.c.* +import a.b + +fun main() {} +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a(); functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.clean b/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.clean new file mode 100644 index 0000000000..3e874ac974 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.clean @@ -0,0 +1,11 @@ +import a.* + +fun foo( + paramA: String, + paramB: Int, + anotherParamIsHere: Float, + myLongParamNameIsHere: CustomType, + lastParam: Boolean, +) = Unit + +fun bar(myLongParamNameIsHereButDoesNotWrap: Int) = Unit diff --git a/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.dirty b/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.dirty new file mode 100644 index 0000000000..ea6e7b2de2 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/trailing-commas.dirty @@ -0,0 +1,8 @@ +import a.* + +fun foo(paramA: String,paramB : Int , anotherParamIsHere: Float ,myLongParamNameIsHere: CustomType +,lastParam: Boolean ) = Unit + +fun bar( + myLongParamNameIsHereButDoesNotWrap: Int, +) = Unit \ No newline at end of file diff --git a/testlib/src/main/resources/kotlin/ktlint/basic-old.clean b/testlib/src/main/resources/kotlin/ktlint/basic-old.clean new file mode 100644 index 0000000000..fd5371bed2 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/basic-old.clean @@ -0,0 +1,5 @@ +fun main() { + fun name() { a(); return b } + println(";") + println() +} diff --git a/testlib/src/main/resources/kotlin/ktlint/basic.clean b/testlib/src/main/resources/kotlin/ktlint/basic.clean index fd5371bed2..b727a7d016 100644 --- a/testlib/src/main/resources/kotlin/ktlint/basic.clean +++ b/testlib/src/main/resources/kotlin/ktlint/basic.clean @@ -1,5 +1,8 @@ fun main() { - fun name() { a(); return b } + fun name() { + a() + return b + } println(";") println() } diff --git a/testlib/src/main/resources/kotlin/ktlint/basic.clean-indent6 b/testlib/src/main/resources/kotlin/ktlint/basic.clean-indent6 new file mode 100644 index 0000000000..cbfbfa8712 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/basic.clean-indent6 @@ -0,0 +1,5 @@ +fun main() { + fun name() { a(); return b } + println(";") + println() +} diff --git a/testlib/src/main/resources/kotlin/ktlint/experimental.clean b/testlib/src/main/resources/kotlin/ktlint/experimental.clean new file mode 100644 index 0000000000..50c41243a8 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/experimental.clean @@ -0,0 +1,7 @@ +fun main() { + if (false) { + println("false") + } else { + println("true") + } +} diff --git a/testlib/src/main/resources/kotlin/ktlint/experimental.dirty b/testlib/src/main/resources/kotlin/ktlint/experimental.dirty new file mode 100644 index 0000000000..cdb6e6bdf5 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/experimental.dirty @@ -0,0 +1,6 @@ +fun main() { + if (false) + println("false") + else + println("true") +} diff --git a/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.clean b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.clean new file mode 100644 index 0000000000..532177d038 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.clean @@ -0,0 +1,5 @@ +fun main() { + val list = listOf( + "hello", + ) +} diff --git a/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.dirty b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.dirty new file mode 100644 index 0000000000..1610d6ba6d --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.dirty @@ -0,0 +1,5 @@ +fun main() { + val list = listOf( + "hello" + ) +} diff --git a/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.ktlintOfficial.clean b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.ktlintOfficial.clean new file mode 100644 index 0000000000..2466bb93fc --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/experimentalEditorConfigOverride.ktlintOfficial.clean @@ -0,0 +1,6 @@ +fun main() { + val list = + listOf( + "hello", + ) +} diff --git a/testlib/src/main/resources/kotlin/ktlint/intellij_idea/.editorconfig b/testlib/src/main/resources/kotlin/ktlint/intellij_idea/.editorconfig new file mode 100644 index 0000000000..9efc62a854 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/intellij_idea/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_code_style = intellij_idea diff --git a/testlib/src/main/resources/kotlin/ktlint/ktlint_official/.editorconfig b/testlib/src/main/resources/kotlin/ktlint/ktlint_official/.editorconfig new file mode 100644 index 0000000000..d66df12e40 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/ktlint_official/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ktlint_code_style = ktlint_official +# See https://github.com/diffplug/spotless/issues/1904. +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2 diff --git a/testlib/src/main/resources/kotlin/ktlint/listScreen.dirty b/testlib/src/main/resources/kotlin/ktlint/listScreen.dirty new file mode 100644 index 0000000000..14c06d6d43 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/listScreen.dirty @@ -0,0 +1,6 @@ +import androidx.compose.runtime.Composable + +@Composable +fun listScreen() { + val list: List = mutableListOf() +} diff --git a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test index c8890b5f34..c78a2f8264 100644 --- a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test +++ b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test @@ -1,5 +1,6 @@ // License Header 2012, 2014 @file:JvmName("SomeFileName") + package my.test object AnObject diff --git a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test index 005124dec9..82c38dec74 100644 --- a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test +++ b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test @@ -1,5 +1,6 @@ // License Header 2012-2014 @file:JvmName("SomeFileName") + package my.test object AnObject2 diff --git a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithoutHeader.test b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithoutHeader.test index 666ba3a652..16ba57b2d4 100644 --- a/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithoutHeader.test +++ b/testlib/src/main/resources/kotlin/licenseheader/KotlinCodeWithoutHeader.test @@ -1,4 +1,5 @@ @file:JvmName("SomeFileName") + package my.test -object AnObject \ No newline at end of file +object AnObject diff --git a/testlib/src/main/resources/license/HelloWorld_java.test b/testlib/src/main/resources/license/HelloWorld_java.test new file mode 100644 index 0000000000..d9be2ea939 --- /dev/null +++ b/testlib/src/main/resources/license/HelloWorld_java.test @@ -0,0 +1,5 @@ +public class HelloWorld { + public static void main(String[] args) { + System.out.print("Hello World"); + } +} diff --git a/testlib/src/main/resources/license/HelloWorld_withImport_java.test b/testlib/src/main/resources/license/HelloWorld_withImport_java.test new file mode 100644 index 0000000000..fcfb64117e --- /dev/null +++ b/testlib/src/main/resources/license/HelloWorld_withImport_java.test @@ -0,0 +1,7 @@ +import java.time.LocalDate; + +public class HelloWorld { + public static void main(String[] args) { + System.out.print("Hello World. Date: " + LocalDate.now()); + } +} diff --git a/testlib/src/main/resources/license/SkipLines.test b/testlib/src/main/resources/license/SkipLines.test new file mode 100644 index 0000000000..4048868ac6 --- /dev/null +++ b/testlib/src/main/resources/license/SkipLines.test @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/testlib/src/main/resources/license/SkipLinesHasLicense.test b/testlib/src/main/resources/license/SkipLinesHasLicense.test new file mode 100644 index 0000000000..f645c5b2e2 --- /dev/null +++ b/testlib/src/main/resources/license/SkipLinesHasLicense.test @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/testlib/src/main/resources/license/module-info.test b/testlib/src/main/resources/license/module-info.test new file mode 100644 index 0000000000..6678147b68 --- /dev/null +++ b/testlib/src/main/resources/license/module-info.test @@ -0,0 +1,5 @@ +module java.sql { + exports java.sql; + exports javax.sql; + exports javax.transaction.xa; +} diff --git a/testlib/src/main/resources/npm/eslint/config/.eslintrc.js b/testlib/src/main/resources/npm/eslint/config/.eslintrc.js new file mode 100644 index 0000000000..6cb3070512 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/config/.eslintrc.js @@ -0,0 +1,38 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "overrides": [ + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +}; diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.configfile.clean b/testlib/src/main/resources/npm/eslint/config/typescript.configfile.clean similarity index 100% rename from testlib/src/main/resources/npm/prettier/config/typescript.configfile.clean rename to testlib/src/main/resources/npm/eslint/config/typescript.configfile.clean diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.defaults.clean b/testlib/src/main/resources/npm/eslint/config/typescript.defaults.clean similarity index 100% rename from testlib/src/main/resources/npm/prettier/config/typescript.defaults.clean rename to testlib/src/main/resources/npm/eslint/config/typescript.defaults.clean diff --git a/testlib/src/main/resources/npm/eslint/config/typescript.dirty b/testlib/src/main/resources/npm/eslint/config/typescript.dirty new file mode 100644 index 0000000000..a3a30bf49a --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/config/typescript.dirty @@ -0,0 +1,10 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary extends AbstractController implements DisposeAware, CallbackAware { + + +public myValue:string[]; + +constructor(private myService:Service,name:string,private field:any){ super(name) ;} + + +//... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.override.clean b/testlib/src/main/resources/npm/eslint/config/typescript.override.clean similarity index 100% rename from testlib/src/main/resources/npm/prettier/config/typescript.override.clean rename to testlib/src/main/resources/npm/eslint/config/typescript.override.clean diff --git a/testlib/src/main/resources/npm/eslint/javascript/custom_rules/.eslintrc.js b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/.eslintrc.js new file mode 100644 index 0000000000..60c5c19f5b --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/.eslintrc.js @@ -0,0 +1,31 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true + }, + "extends": "eslint:recommended", + "overrides": [ + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +}; diff --git a/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.clean b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.clean new file mode 100644 index 0000000000..8bbeb50cf3 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.clean @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined;} + +test ("Hello"); diff --git a/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.dirty b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.dirty new file mode 100644 index 0000000000..36a514cc30 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/custom_rules/javascript-es6.dirty @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: 'Peter', + last : 'Pan', + get fullName() { return this.first + ' ' + this.last; } +}; + +const str = 'Hello, world!' +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = 'Hello \ +World' +; + +function test (a, b = 'world') { let combined =a+ b; return combined}; + +test ('Hello'); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/.eslintrc.js b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/.eslintrc.js new file mode 100644 index 0000000000..d93248ee88 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: 'airbnb-base', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + }, +}; diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.clean b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.clean new file mode 100644 index 0000000000..c8dda3d33f --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.clean @@ -0,0 +1,17 @@ +const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +]; + +const p = { + first: 'Peter', + last: 'Pan', + get fullName() { return `${this.first} ${this.last}`; }, +}; + +const str = 'Hello, world!'; +const str2 = str.charAt(3) + str[0]; + +const multilinestr = 'Hello \ +World'; +function test(a, b = 'world') { const combined = a + b; return combined; } + +test('Hello'); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.dirty b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.dirty new file mode 100644 index 0000000000..08ebafc1a2 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/airbnb/javascript-es6.dirty @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined}; + +test ("Hello"); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/.eslintrc.js b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/.eslintrc.js new file mode 100644 index 0000000000..71b0c47016 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + 'env': { + 'browser': true, + 'es2021': true, + }, + 'extends': 'google', + 'overrides': [ + ], + 'parserOptions': { + 'ecmaVersion': 'latest', + 'sourceType': 'module', + }, + 'rules': { + }, +}; diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.clean b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.clean new file mode 100644 index 0000000000..f960a984c2 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.clean @@ -0,0 +1,25 @@ +const numbers=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +]; + +const p = { + first: 'Peter', + last: 'Pan', + get fullName() { + return this.first + ' ' + this.last; + }, +}; + +const str = 'Hello, world!' +; + +const str2=str.charAt(3)+str[0]; + +const multilinestr = 'Hello \ +World' +; + +function test(a, b = 'world') { + const combined =a+ b; return combined; +}; + +test('Hello'); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.dirty b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.dirty new file mode 100644 index 0000000000..08ebafc1a2 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/google/javascript-es6.dirty @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined}; + +test ("Hello"); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/.eslintrc.js b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/.eslintrc.js new file mode 100644 index 0000000000..cbed25aab9 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + es2021: true + }, + extends: 'standard', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + } +} diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.clean b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.clean new file mode 100644 index 0000000000..757d330a7f --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.clean @@ -0,0 +1,19 @@ +const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 +] + +const p = { + first: 'Peter', + last: 'Pan', + get fullName () { return this.first + ' ' + this.last } +} + +const str = 'Hello, world!' + +const str2 = str.charAt(3) + str[0] + +const multilinestr = 'Hello \ +World' + +function test (a, b = 'world') { const combined = a + b; return combined }; + +test('Hello') diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.dirty b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.dirty new file mode 100644 index 0000000000..08ebafc1a2 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/standard/javascript-es6.dirty @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined}; + +test ("Hello"); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/.eslintrc.js b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/.eslintrc.js new file mode 100644 index 0000000000..844e843c41 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: 'xo', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + rules: { + }, +}; diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.clean b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.clean new file mode 100644 index 0000000000..3cd1bd0865 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.clean @@ -0,0 +1,20 @@ +const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + +const p = { + first: 'Peter', + last: 'Pan', + get fullName() { + return this.first + ' ' + this.last; + }, +}; + +const str = 'Hello, world!'; +const str2 = str.charAt(3) + str[0]; + +const multilinestr = 'Hello \ +World'; +function test(a, b = 'world') { + const combined = a + b; return combined; +} + +test('Hello'); diff --git a/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.dirty b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.dirty new file mode 100644 index 0000000000..08ebafc1a2 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/javascript/styleguide/xo/javascript-es6.dirty @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined}; + +test ("Hello"); diff --git a/testlib/src/main/resources/npm/eslint/typescript/custom_rules/.eslintrc.js b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/.eslintrc.js new file mode 100644 index 0000000000..e6ddff7d4c --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/.eslintrc.js @@ -0,0 +1,59 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "overrides": [ + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "curly": [ + "error" + ], + "max-statements-per-line": [ + "error", + { "max": 1 } + ], + "object-curly-newline": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { "before": false, "after": true } + ], + "object-property-newline": [ + "error", + ], + "no-trailing-spaces": [ + "error" + ], + } +}; diff --git a/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.clean b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.clean new file mode 100644 index 0000000000..526519cae9 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.clean @@ -0,0 +1,11 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + + + public myValue:string[]; + + constructor(private myService:Service, name:string, private field:any){ super(name) ;} + + + //... + +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.dirty b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.dirty new file mode 100644 index 0000000000..0a9201c2e7 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/custom_rules/typescript.dirty @@ -0,0 +1,11 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + + +public myValue:string[]; + +constructor(private myService:Service,name:string,private field:any){ super(name) ;} + + +//... + +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/.eslintrc.js b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/.eslintrc.js new file mode 100644 index 0000000000..0a86d5db86 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/.eslintrc.js @@ -0,0 +1,16 @@ +module.exports = { + env: { + browser: true, + es2021: true + }, + extends: 'standard-with-typescript', + overrides: [ + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', + }, + rules: { + } +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/tsconfig.json b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/tsconfig.json new file mode 100644 index 0000000000..629f834eb5 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "ES6", + "noImplicitAny": true, + "outDir": "build/ide/", + "sourceMap": true, + "skipLibCheck": true, + "target": "ES6", + "baseUrl": "./", + "paths": { + }, + "lib": ["es7", "dom", "es2017"] + }, + "include": [ + "**/*" + ] +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean new file mode 100644 index 0000000000..cbe609b1bb --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.clean @@ -0,0 +1,7 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + public myValue: string[] + + constructor (private readonly myService: Service, name: string, private readonly field: any) { super(name) } + + // ... +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.dirty b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.dirty new file mode 100644 index 0000000000..a8f7447af1 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/standard_with_typescript/typescript.dirty @@ -0,0 +1,10 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + +public myValue:string[]; + +constructor(private myService:Service,name:string,private field:any){ super(name) ;} + + +//... + +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/.eslintrc.js b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/.eslintrc.js new file mode 100644 index 0000000000..e1ca03f732 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: 'xo/browser', + overrides: [ + { + extends: [ + 'xo-typescript', + ], + files: [ + '*.ts', + '*.tsx', + ], + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', + }, + rules: { + }, +}; diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/tsconfig.json b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/tsconfig.json new file mode 100644 index 0000000000..629f834eb5 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "ES6", + "noImplicitAny": true, + "outDir": "build/ide/", + "sourceMap": true, + "skipLibCheck": true, + "target": "ES6", + "baseUrl": "./", + "paths": { + }, + "lib": ["es7", "dom", "es2017"] + }, + "include": [ + "**/*" + ] +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.clean b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.clean new file mode 100644 index 0000000000..5c43d7a746 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.clean @@ -0,0 +1,9 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + public myValue: string[]; + + constructor(private readonly myService: Service, name: string, private readonly field: any) { + super(name); + } + + // ... +} diff --git a/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.dirty b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.dirty new file mode 100644 index 0000000000..a8f7447af1 --- /dev/null +++ b/testlib/src/main/resources/npm/eslint/typescript/styleguide/xo/typescript.dirty @@ -0,0 +1,10 @@ +export class MyController extends AbstractController implements DisposeAware, CallbackAware { + +public myValue:string[]; + +constructor(private myService:Service,name:string,private field:any){ super(name) ;} + + +//... + +} diff --git a/testlib/src/main/resources/npm/prettier/config/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/config/.prettierrc.yml index f078267520..828586c801 100644 --- a/testlib/src/main/resources/npm/prettier/config/.prettierrc.yml +++ b/testlib/src/main/resources/npm/prettier/config/.prettierrc.yml @@ -1,2 +1,2 @@ parser: typescript -printWidth: 50 +printWidth: 20 diff --git a/testlib/src/main/resources/npm/prettier/config/.prettierrc_java_plugin.yml b/testlib/src/main/resources/npm/prettier/config/.prettierrc_java_plugin.yml new file mode 100644 index 0000000000..3f7a801335 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/.prettierrc_java_plugin.yml @@ -0,0 +1,3 @@ +parser: java +tabWidth: 4 +plugins: ['prettier-plugin-java'] diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_2.clean b/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_2.clean new file mode 100644 index 0000000000..451b5466a0 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_2.clean @@ -0,0 +1,18 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary + extends AbstractController + implements + DisposeAware, + CallbackAware +{ + public myValue: string[]; + + constructor( + private myService: Service, + name: string, + private field: any + ) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_3.clean b/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_3.clean new file mode 100644 index 0000000000..30d30f8595 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.configfile_prettier_3.clean @@ -0,0 +1,18 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary + extends AbstractController + implements + DisposeAware, + CallbackAware +{ + public myValue: string[]; + + constructor( + private myService: Service, + name: string, + private field: any, + ) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_2.clean b/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_2.clean new file mode 100644 index 0000000000..61d8e020d6 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_2.clean @@ -0,0 +1,12 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary + extends AbstractController + implements DisposeAware, CallbackAware +{ + public myValue: string[]; + + constructor(private myService: Service, name: string, private field: any) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_3.clean b/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_3.clean new file mode 100644 index 0000000000..9fa0eed3cc --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.defaults_prettier_3.clean @@ -0,0 +1,16 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary + extends AbstractController + implements DisposeAware, CallbackAware +{ + public myValue: string[]; + + constructor( + private myService: Service, + name: string, + private field: any, + ) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_2.clean b/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_2.clean new file mode 100644 index 0000000000..3f3a8c30af --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_2.clean @@ -0,0 +1,9 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary extends AbstractController implements DisposeAware, CallbackAware { + public myValue: string[]; + + constructor(private myService: Service, name: string, private field: any) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_3.clean b/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_3.clean new file mode 100644 index 0000000000..c49cd544df --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/config/typescript.override_prettier_3.clean @@ -0,0 +1,13 @@ +export class MyVeryOwnControllerWithARatherLongNameThatIsNotReallyNecessary extends AbstractController implements DisposeAware, CallbackAware { + public myValue: string[]; + + constructor( + private myService: Service, + name: string, + private field: any, + ) { + super(name); + } + + //... +} diff --git a/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/.prettierrc.yml new file mode 100644 index 0000000000..f1185441f8 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/.prettierrc.yml @@ -0,0 +1 @@ +parser: html diff --git a/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.clean b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.clean new file mode 100644 index 0000000000..385e636b7b --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.clean @@ -0,0 +1,13 @@ + + + + + Test + + + + + + + + diff --git a/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.dirty b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.dirty new file mode 100644 index 0000000000..35ed1e9d3b --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/html_prettier3/html_prettier3.dirty @@ -0,0 +1,17 @@ + + + + + + Test + + + + + + + + + diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean index 29002f6b05..0cfac613f1 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean +++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean @@ -1,24 +1,5 @@ var numbers = [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]; var p = { @@ -32,7 +13,8 @@ var p = { var str = "Hello, world!"; var str2 = str.charAt(3) + str[0]; -var multilinestr = "Hello \ +var multilinestr = + "Hello \ World"; function test(a, b) { return a + b; diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean index d4e982d69f..1935faa03c 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean +++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean @@ -1,24 +1,5 @@ var numbers = [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ]; const p = { @@ -32,7 +13,8 @@ const p = { const str = "Hello, world!"; var str2 = str.charAt(3) + str[0]; -var multilinestr = "Hello \ +var multilinestr = + "Hello \ World"; function test(a, b = "world") { let combined = a + b; diff --git a/testlib/src/main/resources/npm/prettier/plugins/java-test.clean b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean index c45ffe5183..93cf43955f 100644 --- a/testlib/src/main/resources/npm/prettier/plugins/java-test.clean +++ b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean @@ -4,6 +4,7 @@ import java.util.List; import java.util.function.Consumer; public class JavaTest { + private static final String NAME = "JavaTest"; private List strings = new ArrayList<>(); @@ -30,8 +31,8 @@ public class JavaTest { JavaTest javaTest = new JavaTest("1", "2", "3"); System.out.println("joined: " + javaTest.join(',')); StringBuilder builder = new StringBuilder(); - javaTest.operateOn( - strings -> builder.append(String.join("---", strings)) + javaTest.operateOn(strings -> + builder.append(String.join("---", strings)) ); } } diff --git a/testlib/src/main/resources/npm/prettier/plugins/php.clean b/testlib/src/main/resources/npm/prettier/plugins/php.clean index 4c8710bbee..067647a029 100644 --- a/testlib/src/main/resources/npm/prettier/plugins/php.clean +++ b/testlib/src/main/resources/npm/prettier/plugins/php.clean @@ -27,17 +27,17 @@ abstract class ReallyReallyReallyLongClassName // variable doc public $test; public $other = 1; - public static $staticTest = ['hi']; + public static $staticTest = ["hi"]; static $cache; protected static $_instance; - protected $fillable = ['title', 'requester_id', 'type', 'summary', 'proof']; + protected $fillable = ["title", "requester_id", "type", "summary", "proof"]; protected $fillable2 = [ - 'title', - 'description', - 'requester_id', - 'type', - 'summary', - 'proof', + "title", + "description", + "requester_id", + "type", + "summary", + "proof", ]; protected $test = [ //test @@ -52,7 +52,7 @@ abstract class ReallyReallyReallyLongClassName * * @return \Some\Test */ - public function __construct($test, $test_int = null, $test_string = 'hi') + public function __construct($test, $test_int = null, $test_string = "hi") { parent::__construct($test_int ?: 1); $this->other = $test_string; @@ -108,7 +108,7 @@ abstract class ReallyReallyReallyLongClassName public function returnTypeTest(): string { - return 'hi'; + return "hi"; } final public static function bar() @@ -125,17 +125,17 @@ abstract class ReallyReallyReallyLongClassName public function method1() { - return 'hi'; + return "hi"; } public function method2() { - return 'hi'; + return "hi"; } public function method3() { - return 'hi'; + return "hi"; } public function testReturn(?string $name): ?string @@ -164,7 +164,7 @@ abstract class ReallyReallyReallyLongClassName string $bar, int $baz ): string { - return 'foo'; + return "foo"; } public function longLongAnotherFunctionOther( @@ -172,7 +172,7 @@ abstract class ReallyReallyReallyLongClassName string $bar, int $baz ) { - return 'foo'; + return "foo"; } public function testReturnTypeDeclaration(): object @@ -297,13 +297,13 @@ class field extends \models\base { protected function pre_save($input, $fields) { - $input['configs'] = json_encode( + $input["configs"] = json_encode( array_merge( $configs, - $field_type->process_field_config_from_user($input['definition']) + $field_type->process_field_config_from_user($input["definition"]) ) ); - unset($input['definition']); + unset($input["definition"]); } } @@ -311,8 +311,8 @@ class test { public function test_method() { - $customer = (object) ['name' => 'Bob']; - $job = (object) ['customer' => $customer]; + $customer = (object) ["name" => "Bob"]; + $job = (object) ["customer" => $customer]; return "The customer for that job, {$job->customer->name} has an error that shows up after the line gets waaaaay toooo long."; } @@ -488,9 +488,9 @@ class User { public int $id; public string $name; - public ?string $b = 'foo'; + public ?string $b = "foo"; private Foo $prop; - protected static string $static = 'default'; + protected static string $static = "default"; public function __construct(int $id, string $name) { diff --git a/testlib/src/main/resources/protobuf/buf/buf.proto b/testlib/src/main/resources/protobuf/buf/buf.proto new file mode 100644 index 0000000000..a90d58bbd0 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf.proto @@ -0,0 +1,5 @@ +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} \ No newline at end of file diff --git a/testlib/src/main/resources/protobuf/buf/buf.proto.clean b/testlib/src/main/resources/protobuf/buf/buf.proto.clean new file mode 100644 index 0000000000..faa8d91f24 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf.proto.clean @@ -0,0 +1,5 @@ +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} diff --git a/testlib/src/main/resources/protobuf/buf/buf_large.proto b/testlib/src/main/resources/protobuf/buf/buf_large.proto new file mode 100644 index 0000000000..01a638d726 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf_large.proto @@ -0,0 +1,229 @@ +syntax = "proto3"; + +package com.diffplug.gradle.spotless.buf.proto; + +option java_multiple_files = true; + +message Message { + string message = 1; +} + +service Services { +rpc Echo(Message) returns (Message); +} + +message Message01 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message02 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message03 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message04 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message05 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message06 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message07 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message08 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message09 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message10 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message11 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message12 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message13 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message14 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message15 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message16 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message17 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message18 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; +string message9 = 9; +} diff --git a/testlib/src/main/resources/protobuf/buf/buf_large.proto.clean b/testlib/src/main/resources/protobuf/buf/buf_large.proto.clean new file mode 100644 index 0000000000..c4218adfc8 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf_large.proto.clean @@ -0,0 +1,229 @@ +syntax = "proto3"; + +package com.diffplug.gradle.spotless.buf.proto; + +option java_multiple_files = true; + +message Message { + string message = 1; +} + +service Services { + rpc Echo(Message) returns (Message); +} + +message Message01 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message02 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message03 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message04 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message05 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message06 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message07 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message08 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message09 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message10 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message11 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message12 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message13 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message14 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message15 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message16 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message17 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} + +message Message18 { + string message1 = 1; + string message2 = 2; + string message3 = 3; + string message4 = 4; + string message5 = 5; + string message6 = 6; + string message7 = 7; + string message8 = 8; + string message9 = 9; +} diff --git a/testlib/src/main/resources/protobuf/buf/license.proto b/testlib/src/main/resources/protobuf/buf/license.proto new file mode 100644 index 0000000000..aabedc5093 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/license.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} diff --git a/testlib/src/main/resources/protobuf/buf/license.proto.clean b/testlib/src/main/resources/protobuf/buf/license.proto.clean new file mode 100644 index 0000000000..11761efc28 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/license.proto.clean @@ -0,0 +1,8 @@ +/* (C) 2022 */ +syntax = "proto3"; + +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} 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/v1.2.12-default/shacl-shacl.ttl new file mode 100644 index 0000000000..5dff7d1980 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl-shacl.ttl @@ -0,0 +1,432 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix sh: . +@prefix shsh: . + +shsh: rdfs:label "SHACL for SHACL"@en ; + rdfs:comment "This shapes graph can be used to validate SHACL shapes graphs against a subset of the syntax rules."@en ; + sh:declare [ + sh:namespace "http://www.w3.org/ns/shacl-shacl#" ; + sh:prefix "shsh" ; + ] . + +shsh:EntailmentShape a sh:NodeShape ; + sh:nodeKind sh:IRI ; + sh:targetObjectsOf sh:entailment . + +shsh:ListNodeShape a sh:NodeShape ; + rdfs:label "List node shape"@en ; + rdfs:comment "Defines constraints on what it means for a node to be a node within a well-formed RDF list. Note that this does not check whether the rdf:rest items are also well-formed lists as this would lead to unsupported recursion."@en ; + sh:or ( [ + sh:hasValue ( ) ; + sh:property [ + sh:maxCount 0 ; + sh:path rdf:first ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path rdf:rest ; + ] ; + ] [ + sh:not [ + sh:hasValue ( ) ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path rdf:first ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path rdf:rest ; + ] ; + ] ) . + +shsh:ListShape a sh:NodeShape ; + rdfs:label "List shape"@en ; + rdfs:comment "A shape describing well-formed RDF lists. Currently does not check for non-recursion. This could be expressed using SHACL-SPARQL."@en ; + rdfs:seeAlso ; + sh:property [ + rdfs:comment "Each list member (including this node) must be have the shape shsh:ListNodeShape."@en ; + sh:hasValue ( ) ; + sh:node shsh:ListNodeShape ; + sh:path [ + sh:zeroOrMorePath rdf:rest ; + ] ; + ] . + +shsh:NodeShapeShape a sh:NodeShape ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:path ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:lessThan ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:lessThanOrEquals ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:maxCount ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:minCount ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:qualifiedValueShape ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:uniqueLang ; + ] ; + sh:targetObjectsOf sh:node . + +shsh:PathListWithAtLeast2Members a sh:NodeShape ; + sh:node shsh:ListShape ; + sh:property [ + sh:minCount 2 ; + sh:path [ + sh:oneOrMorePath rdf:rest ; + ] ; + ] . + +shsh:PathNodeShape sh:xone ( [ + sh:nodeKind sh:IRI ; + ] [ + sh:node shsh:PathListWithAtLeast2Members ; + sh:nodeKind sh:BlankNode ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:node shsh:PathListWithAtLeast2Members ; + sh:path sh:alternativePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:inversePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:zeroOrMorePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:oneOrMorePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:zeroOrOnePath ; + ] ; + ] ) . + +shsh:PathShape a sh:NodeShape ; + rdfs:label "Path shape"@en ; + rdfs:comment "A shape that can be used to validate the syntax rules of well-formed SHACL paths."@en ; + rdfs:seeAlso ; + sh:property [ + sh:node shsh:PathNodeShape ; + sh:path [ + sh:zeroOrMorePath [ + sh:alternativePath ( ( [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ( sh:alternativePath [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) sh:inversePath sh:zeroOrMorePath sh:oneOrMorePath sh:zeroOrOnePath ) ; + ] ; + ] ; + ] . + +shsh:PropertyShapeShape a sh:NodeShape ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:node shsh:PathShape ; + sh:path sh:path ; + ] ; + sh:targetObjectsOf sh:property . + +shsh:ShapeShape a sh:NodeShape ; + rdfs:label "Shape shape"@en ; + rdfs:comment "A shape that can be used to validate syntax rules for other shapes."@en ; + sh:or ( [ + sh:not [ + sh:class rdfs:Class ; + sh:or ( [ + sh:class sh:NodeShape ; + ] [ + sh:class sh:PropertyShape ; + ] ) ; + ] ; + ] [ + sh:nodeKind sh:IRI ; + ] ) ; + sh:property [ + sh:nodeKind sh:IRIOrLiteral ; + sh:path sh:targetNode ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetClass ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetSubjectsOf ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetObjectsOf ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:severity ; + ] ; + sh:property [ + sh:or ( [ + sh:datatype xsd:string ; + ] [ + sh:datatype rdf:langString ; + ] ) ; + sh:path sh:message ; + ] ; + sh:property [ + sh:in ( true false ) ; + sh:maxCount 1 ; + sh:path sh:deactivated ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:and ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:class ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:closed ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:ignoredProperties ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path ( sh:ignoredProperties [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:datatype ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:disjoint ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:equals ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:in ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:languageIn ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:path ( sh:languageIn [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:lessThan ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:lessThanOrEquals ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxExclusive ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxInclusive ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxLength ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minExclusive ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minInclusive ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minLength ; + ] ; + sh:property [ + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; + sh:maxCount 1 ; + sh:path sh:nodeKind ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:or ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path sh:pattern ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path sh:flags ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:qualifiedMaxCount ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:qualifiedMinCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:path sh:qualifiedValueShape ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:qualifiedValueShapesDisjoint ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:uniqueLang ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:xone ; + ] ; + sh:targetClass sh:NodeShape ; + sh:targetClass sh:PropertyShape ; + sh:targetObjectsOf sh:node ; + sh:targetObjectsOf sh:not ; + sh:targetObjectsOf sh:property ; + sh:targetObjectsOf sh:qualifiedValueShape ; + sh:targetSubjectsOf sh:and ; + sh:targetSubjectsOf sh:class ; + sh:targetSubjectsOf sh:closed ; + sh:targetSubjectsOf sh:datatype ; + sh:targetSubjectsOf sh:disjoint ; + sh:targetSubjectsOf sh:equals ; + sh:targetSubjectsOf sh:flags ; + sh:targetSubjectsOf sh:hasValue ; + sh:targetSubjectsOf sh:ignoredProperties ; + sh:targetSubjectsOf sh:in ; + sh:targetSubjectsOf sh:languageIn ; + sh:targetSubjectsOf sh:lessThan ; + sh:targetSubjectsOf sh:lessThanOrEquals ; + sh:targetSubjectsOf sh:maxCount ; + sh:targetSubjectsOf sh:maxExclusive ; + sh:targetSubjectsOf sh:maxInclusive ; + sh:targetSubjectsOf sh:maxLength ; + sh:targetSubjectsOf sh:minCount ; + sh:targetSubjectsOf sh:minExclusive ; + sh:targetSubjectsOf sh:minInclusive ; + sh:targetSubjectsOf sh:minLength ; + sh:targetSubjectsOf sh:node ; + sh:targetSubjectsOf sh:nodeKind ; + sh:targetSubjectsOf sh:not ; + sh:targetSubjectsOf sh:or ; + sh:targetSubjectsOf sh:pattern ; + sh:targetSubjectsOf sh:property ; + sh:targetSubjectsOf sh:qualifiedMaxCount ; + sh:targetSubjectsOf sh:qualifiedMinCount ; + sh:targetSubjectsOf sh:qualifiedValueShape ; + sh:targetSubjectsOf sh:qualifiedValueShapesDisjoint ; + sh:targetSubjectsOf sh:sparql ; + sh:targetSubjectsOf sh:targetClass ; + sh:targetSubjectsOf sh:targetNode ; + sh:targetSubjectsOf sh:targetObjectsOf ; + sh:targetSubjectsOf sh:targetSubjectsOf ; + sh:targetSubjectsOf sh:uniqueLang ; + sh:targetSubjectsOf sh:xone ; + sh:xone ( shsh:NodeShapeShape shsh:PropertyShapeShape ) . + +shsh:ShapesGraphShape a sh:NodeShape ; + sh:nodeKind sh:IRI ; + sh:targetObjectsOf sh:shapesGraph . + +shsh:ShapesListShape a sh:NodeShape ; + sh:property [ + sh:node shsh:ShapeShape ; + sh:path ( [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:targetObjectsOf sh:and ; + sh:targetObjectsOf sh:or ; + sh:targetObjectsOf sh:xone . + + diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl.ttl new file mode 100644 index 0000000000..0b6359813d --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-default/shacl.ttl @@ -0,0 +1,1346 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix owl: . +@prefix sh: . + +sh: a owl:Ontology ; + rdfs:label "W3C Shapes Constraint Language (SHACL) Vocabulary"@en ; + rdfs:comment "This vocabulary defines terms used in SHACL, the W3C Shapes Constraint Language."@en ; + sh:declare [ + sh:namespace "http://www.w3.org/ns/shacl#" ; + sh:prefix "sh" ; + ] ; + sh:suggestedShapesGraph . + +sh:AbstractResult a rdfs:Class ; + rdfs:label "Abstract result"@en ; + rdfs:comment "The base class of validation results, typically not instantiated directly."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:ConstraintComponent a rdfs:Class ; + rdfs:label "Constraint component"@en ; + rdfs:comment "The class of constraint components."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Parameterizable . + +sh:Function a rdfs:Class ; + rdfs:label "Function"@en ; + rdfs:comment "The class of SHACL functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Parameterizable . + +sh:JSConstraint a rdfs:Class ; + rdfs:label "JavaScript-based constraint"@en ; + rdfs:comment "The class of constraints backed by a JavaScript function."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable . + +sh:JSExecutable a rdfs:Class ; + rdfs:label "JavaScript executable"@en ; + rdfs:comment "Abstract base class of resources that declare an executable JavaScript."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:JSFunction a rdfs:Class ; + rdfs:label "JavaScript function"@en ; + rdfs:comment "The class of SHACL functions that execute a JavaScript function when called."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:JSExecutable . + +sh:JSLibrary a rdfs:Class ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Represents a JavaScript library, typically identified by one or more URLs of files to include."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:JSRule a rdfs:Class ; + rdfs:label "JavaScript rule"@en ; + rdfs:comment "The class of SHACL rules expressed using JavaScript."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Rule . + +sh:JSTarget a rdfs:Class ; + rdfs:label "JavaScript target"@en ; + rdfs:comment "The class of targets that are based on JavaScript functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Target . + +sh:JSTargetType a rdfs:Class ; + rdfs:label "JavaScript target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on JavaScript functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:TargetType . + +sh:JSValidator a rdfs:Class ; + rdfs:label "JavaScript validator"@en ; + rdfs:comment "A SHACL validator based on JavaScript. This can be used to declare SHACL constraint components that perform JavaScript-based validation when used."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Validator . + +sh:NodeKind a rdfs:Class ; + rdfs:label "Node kind"@en ; + rdfs:comment "The class of all node kinds, including sh:BlankNode, sh:IRI, sh:Literal or the combinations of these: sh:BlankNodeOrIRI, sh:BlankNodeOrLiteral, sh:IRIOrLiteral."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:NodeShape a rdfs:Class ; + rdfs:label "Node shape"@en ; + rdfs:comment "A node shape is a shape that specifies constraint that need to be met with respect to focus nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Shape . + +sh:Parameter a rdfs:Class ; + rdfs:label "Parameter"@en ; + rdfs:comment "The class of parameter declarations, consisting of a path predicate and (possibly) information about allowed value type, cardinality and other characteristics."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:PropertyShape . + +sh:Parameterizable a rdfs:Class ; + rdfs:label "Parameterizable"@en ; + rdfs:comment "Superclass of components that can take parameters, especially functions and constraint components."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PrefixDeclaration a rdfs:Class ; + rdfs:label "Prefix declaration"@en ; + rdfs:comment "The class of prefix declarations, consisting of pairs of a prefix with a namespace."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PropertyGroup a rdfs:Class ; + rdfs:label "Property group"@en ; + rdfs:comment "Instances of this class represent groups of property shapes that belong together."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PropertyShape a rdfs:Class ; + rdfs:label "Property shape"@en ; + rdfs:comment "A property shape is a shape that specifies constraints on the values of a focus node for a given property or path."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Shape . + +sh:ResultAnnotation a rdfs:Class ; + rdfs:label "Result annotation"@en ; + rdfs:comment "A class of result annotations, which define the rules to derive the values of a given annotation property as extra values for a validation result."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Rule a rdfs:Class ; + rdfs:label "Rule"@en ; + rdfs:comment "The class of SHACL rules. Never instantiated directly."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:SPARQLAskExecutable a rdfs:Class ; + rdfs:label "SPARQL ASK executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on an ASK query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLAskValidator a rdfs:Class ; + rdfs:label "SPARQL ASK validator"@en ; + rdfs:comment "The class of validators based on SPARQL ASK queries. The queries are evaluated for each value node and are supposed to return true if the given node conforms."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:Validator . + +sh:SPARQLConstraint a rdfs:Class ; + rdfs:label "SPARQL constraint"@en ; + rdfs:comment "The class of constraints based on SPARQL SELECT queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLSelectExecutable . + +sh:SPARQLConstructExecutable a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on a CONSTRUCT query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLExecutable a rdfs:Class ; + rdfs:label "SPARQL executable"@en ; + rdfs:comment "The class of resources that encapsulate a SPARQL query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:SPARQLFunction a rdfs:Class ; + rdfs:label "SPARQL function"@en ; + rdfs:comment "A function backed by a SPARQL query - either ASK or SELECT."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable . + +sh:SPARQLRule a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT rule"@en ; + rdfs:comment "The class of SHACL rules based on SPARQL CONSTRUCT queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Rule ; + rdfs:subClassOf sh:SPARQLConstructExecutable . + +sh:SPARQLSelectExecutable a rdfs:Class ; + rdfs:label "SPARQL SELECT executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SELECT query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLSelectValidator a rdfs:Class ; + rdfs:label "SPARQL SELECT validator"@en ; + rdfs:comment "The class of validators based on SPARQL SELECT queries. The queries are evaluated for each focus node and are supposed to produce bindings for all focus nodes that do not conform."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:Validator . + +sh:SPARQLTarget a rdfs:Class ; + rdfs:label "SPARQL target"@en ; + rdfs:comment "The class of targets that are based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:Target . + +sh:SPARQLTargetType a rdfs:Class ; + rdfs:label "SPARQL target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:TargetType . + +sh:SPARQLUpdateExecutable a rdfs:Class ; + rdfs:label "SPARQL UPDATE executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SPARQL UPDATE."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:Severity a rdfs:Class ; + rdfs:label "Severity"@en ; + rdfs:comment "The class of validation result severity levels, including violation and warning levels."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Shape a rdfs:Class ; + rdfs:label "Shape"@en ; + rdfs:comment "A shape is a collection of constraints that may be targeted for certain nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Target a rdfs:Class ; + rdfs:label "Target"@en ; + rdfs:comment "The base class of targets such as those based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:TargetType a rdfs:Class ; + rdfs:label "Target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets.\tInstances of this are instantiated as values of the sh:target property."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Class ; + rdfs:subClassOf sh:Parameterizable . + +sh:TripleRule a rdfs:Class ; + rdfs:label "A rule based on triple (subject, predicate, object) pattern."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Rule . + +sh:ValidationReport a rdfs:Class ; + rdfs:label "Validation report"@en ; + rdfs:comment "The class of SHACL validation reports."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:ValidationResult a rdfs:Class ; + rdfs:label "Validation result"@en ; + rdfs:comment "The class of validation results."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:AbstractResult . + +sh:Validator a rdfs:Class ; + rdfs:label "Validator"@en ; + rdfs:comment "The class of validators, which provide instructions on how to process a constraint definition. This class serves as base class for the SPARQL-based validators and other possible implementations."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:alternativePath a rdf:Property ; + rdfs:label "alternative path"@en ; + rdfs:comment "The (single) value of this property must be a list of path elements, representing the elements of alternative paths."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:and a rdf:Property ; + rdfs:label "and"@en ; + rdfs:comment "RDF list of shapes to validate the value nodes against."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:annotationProperty a rdf:Property ; + rdfs:label "annotation property"@en ; + rdfs:comment "The annotation property that shall be set."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:annotationValue a rdf:Property ; + rdfs:label "annotation value"@en ; + rdfs:comment "The (default) values of the annotation property."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: . + +sh:annotationVarName a rdf:Property ; + rdfs:label "annotation variable name"@en ; + rdfs:comment "The name of the SPARQL variable from the SELECT clause that shall be used for the values."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:ask a rdf:Property ; + rdfs:label "ask"@en ; + rdfs:comment "The SPARQL ASK query to execute."@en ; + rdfs:domain sh:SPARQLAskExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:class a rdf:Property ; + rdfs:label "class"@en ; + rdfs:comment "The type that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:closed a rdf:Property ; + rdfs:label "closed"@en ; + rdfs:comment "If set to true then the shape is closed."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:condition a rdf:Property ; + rdfs:label "condition"@en ; + rdfs:comment "The shapes that the focus nodes need to conform to before a rule is executed on them."@en ; + rdfs:domain sh:Rule ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:conforms a rdf:Property ; + rdfs:label "conforms"@en ; + rdfs:comment "True if the validation did not produce any validation results, and false otherwise."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:construct a rdf:Property ; + rdfs:label "construct"@en ; + rdfs:comment "The SPARQL CONSTRUCT query to execute."@en ; + rdfs:domain sh:SPARQLConstructExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:datatype a rdf:Property ; + rdfs:label "datatype"@en ; + rdfs:comment "Specifies an RDF datatype that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Datatype . + +sh:deactivated a rdf:Property ; + rdfs:label "deactivated"@en ; + rdfs:comment "If set to true then all nodes conform to this."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:declare a rdf:Property ; + rdfs:label "declare"@en ; + rdfs:comment "Links a resource with its namespace prefix declarations."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PrefixDeclaration . + +sh:defaultValue a rdf:Property ; + rdfs:label "default value"@en ; + rdfs:comment "A default value for a property, for example for user interface tools to pre-populate input fields."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:description a rdf:Property ; + rdfs:label "description"@en ; + rdfs:comment "Human-readable descriptions for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:detail a rdf:Property ; + rdfs:label "detail"@en ; + rdfs:comment "Links a result with other results that provide more details, for example to describe violations against nested shapes."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:AbstractResult . + +sh:disjoint a rdf:Property ; + rdfs:label "disjoint"@en ; + rdfs:comment "Specifies a property where the set of values must be disjoint with the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:entailment a rdf:Property ; + rdfs:label "entailment"@en ; + rdfs:comment "An entailment regime that indicates what kind of inferencing is required by a shapes graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:equals a rdf:Property ; + rdfs:label "equals"@en ; + rdfs:comment "Specifies a property that must have the same values as the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:expression a rdf:Property ; + rdfs:label "expression"@en ; + rdfs:comment "The node expression that must return true for the value nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:filterShape a rdf:Property ; + rdfs:label "filter shape"@en ; + rdfs:comment "The shape that all input nodes of the expression need to conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:flags a rdf:Property ; + rdfs:label "flags"@en ; + rdfs:comment "An optional flag to be used with regular expression pattern matching."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:focusNode a rdf:Property ; + rdfs:label "focus node"@en ; + rdfs:comment "The focus node that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:group a rdf:Property ; + rdfs:label "group"@en ; + rdfs:comment "Can be used to link to a property group to indicate that a property shape belongs to a group of related property shapes."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PropertyGroup . + +sh:hasValue a rdf:Property ; + rdfs:label "has value"@en ; + rdfs:comment "Specifies a value that must be among the value nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:ignoredProperties a rdf:Property ; + rdfs:label "ignored properties"@en ; + rdfs:comment "An optional RDF list of properties that are also permitted in addition to those explicitly enumerated via sh:property/sh:path."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:in a rdf:Property ; + rdfs:label "in"@en ; + rdfs:comment "Specifies a list of allowed values so that each value node must be among the members of the given list."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:intersection a rdf:Property ; + rdfs:label "intersection"@en ; + rdfs:comment "A list of node expressions that shall be intersected."@en ; + rdfs:isDefinedBy sh: . + +sh:inversePath a rdf:Property ; + rdfs:label "inverse path"@en ; + rdfs:comment "The (single) value of this property represents an inverse path (object to subject)."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:js a rdf:Property ; + rdfs:label "JavaScript constraint"@en ; + rdfs:comment "Constraints expressed in JavaScript." ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:JSConstraint . + +sh:jsFunctionName a rdf:Property ; + rdfs:label "JavaScript function name"@en ; + rdfs:comment "The name of the JavaScript function to execute."@en ; + rdfs:domain sh:JSExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:jsLibrary a rdf:Property ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Declares which JavaScript libraries are needed to execute this."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:JSLibrary . + +sh:jsLibraryURL a rdf:Property ; + rdfs:label "JavaScript library URL"@en ; + rdfs:comment "Declares the URLs of a JavaScript library. This should be the absolute URL of a JavaScript file. Implementations may redirect those to local files."@en ; + rdfs:domain sh:JSLibrary ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:anyURI . + +sh:labelTemplate a rdf:Property ; + rdfs:label "label template"@en ; + rdfs:comment "Outlines how human-readable labels of instances of the associated Parameterizable shall be produced. The values can contain {?paramName} as placeholders for the actual values of the given parameter."@en ; + rdfs:domain sh:Parameterizable ; + rdfs:isDefinedBy sh: . + +sh:languageIn a rdf:Property ; + rdfs:label "language in"@en ; + rdfs:comment "Specifies a list of language tags that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:lessThan a rdf:Property ; + rdfs:label "less than"@en ; + rdfs:comment "Specifies a property that must have smaller values than the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:lessThanOrEquals a rdf:Property ; + rdfs:label "less than or equals"@en ; + rdfs:comment "Specifies a property that must have smaller or equal values than the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:maxCount a rdf:Property ; + rdfs:label "max count"@en ; + rdfs:comment "Specifies the maximum number of values in the set of value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:maxExclusive a rdf:Property ; + rdfs:label "max exclusive"@en ; + rdfs:comment "Specifies the maximum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:maxInclusive a rdf:Property ; + rdfs:label "max inclusive"@en ; + rdfs:comment "Specifies the maximum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:maxLength a rdf:Property ; + rdfs:label "max length"@en ; + rdfs:comment "Specifies the maximum string length of each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:message a rdf:Property ; + rdfs:label "message"@en ; + rdfs:comment "A human-readable message (possibly with placeholders for variables) explaining the cause of the result."@en ; + rdfs:isDefinedBy sh: . + +sh:minCount a rdf:Property ; + rdfs:label "min count"@en ; + rdfs:comment "Specifies the minimum number of values in the set of value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:minExclusive a rdf:Property ; + rdfs:label "min exclusive"@en ; + rdfs:comment "Specifies the minimum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:minInclusive a rdf:Property ; + rdfs:label "min inclusive"@en ; + rdfs:comment "Specifies the minimum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:minLength a rdf:Property ; + rdfs:label "min length"@en ; + rdfs:comment "Specifies the minimum string length of each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:name a rdf:Property ; + rdfs:label "name"@en ; + rdfs:comment "Human-readable labels for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:namespace a rdf:Property ; + rdfs:label "namespace"@en ; + rdfs:comment "The namespace associated with a prefix in a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:anyURI . + +sh:node a rdf:Property ; + rdfs:label "node"@en ; + rdfs:comment "Specifies the node shape that all value nodes must conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:NodeShape . + +sh:nodeKind a rdf:Property ; + rdfs:label "node kind"@en ; + rdfs:comment "Specifies the node kind (e.g. IRI or literal) each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:NodeKind . + +sh:nodeValidator a rdf:Property ; + rdfs:label "shape validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a node shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:nodes a rdf:Property ; + rdfs:label "nodes"@en ; + rdfs:comment "The node expression producing the input nodes of a filter shape expression."@en ; + rdfs:isDefinedBy sh: . + +sh:not a rdf:Property ; + rdfs:label "not"@en ; + rdfs:comment "Specifies a shape that the value nodes must not conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:object a rdf:Property ; + rdfs:label "object"@en ; + rdfs:comment "An expression producing the nodes that shall be inferred as objects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:oneOrMorePath a rdf:Property ; + rdfs:label "one or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched one or more times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:optional a rdf:Property ; + rdfs:label "optional"@en ; + rdfs:comment "Indicates whether a parameter is optional."@en ; + rdfs:domain sh:Parameter ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:or a rdf:Property ; + rdfs:label "or"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to at least one of the shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:order a rdf:Property ; + rdfs:label "order"@en ; + rdfs:comment "Specifies the relative order of this compared to its siblings. For example use 0 for the first, 1 for the second."@en ; + rdfs:isDefinedBy sh: . + +sh:parameter a rdf:Property ; + rdfs:label "parameter"@en ; + rdfs:comment "The parameters of a function or constraint component."@en ; + rdfs:domain sh:Parameterizable ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Parameter . + +sh:path a rdf:Property ; + rdfs:label "path"@en ; + rdfs:comment "Specifies the property path of a property shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:pattern a rdf:Property ; + rdfs:label "pattern"@en ; + rdfs:comment "Specifies a regular expression pattern that the string representations of the value nodes must match."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:predicate a rdf:Property ; + rdfs:label "predicate"@en ; + rdfs:comment "An expression producing the properties that shall be inferred as predicates."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:prefix a rdf:Property ; + rdfs:label "prefix"@en ; + rdfs:comment "The prefix of a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:prefixes a rdf:Property ; + rdfs:label "prefixes"@en ; + rdfs:comment "The prefixes that shall be applied before parsing the associated SPARQL query."@en ; + rdfs:domain sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:property a rdf:Property ; + rdfs:label "property"@en ; + rdfs:comment "Links a shape to its property shapes."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PropertyShape . + +sh:propertyValidator a rdf:Property ; + rdfs:label "property validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a property shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:qualifiedMaxCount a rdf:Property ; + rdfs:label "qualified max count"@en ; + rdfs:comment "The maximum number of value nodes that can conform to the shape."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:qualifiedMinCount a rdf:Property ; + rdfs:label "qualified min count"@en ; + rdfs:comment "The minimum number of value nodes that must conform to the shape."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:qualifiedValueShape a rdf:Property ; + rdfs:label "qualified value shape"@en ; + rdfs:comment "The shape that a specified number of values must conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:qualifiedValueShapesDisjoint a rdf:Property ; + rdfs:label "qualified value shapes disjoint"@en ; + rdfs:comment "Can be used to mark the qualified value shape to be disjoint with its sibling shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:result a rdf:Property ; + rdfs:label "result"@en ; + rdfs:comment "The validation results contained in a validation report."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ValidationResult . + +sh:resultAnnotation a rdf:Property ; + rdfs:label "result annotation"@en ; + rdfs:comment "Links a SPARQL validator with zero or more sh:ResultAnnotation instances, defining how to derive additional result properties based on the variables of the SELECT query."@en ; + rdfs:domain sh:SPARQLSelectValidator ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ResultAnnotation . + +sh:resultMessage a rdf:Property ; + rdfs:label "result message"@en ; + rdfs:comment "Human-readable messages explaining the cause of the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:resultPath a rdf:Property ; + rdfs:label "result path"@en ; + rdfs:comment "The path of a validation result, based on the path of the validated property shape."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:resultSeverity a rdf:Property ; + rdfs:label "result severity"@en ; + rdfs:comment "The severity of the result, e.g. warning."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Severity . + +sh:returnType a rdf:Property ; + rdfs:label "return type"@en ; + rdfs:comment "The expected type of values returned by the associated function."@en ; + rdfs:domain sh:Function ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:rule a rdf:Property ; + rdfs:label "rule"@en ; + rdfs:comment "The rules linked to a shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Rule . + +sh:select a rdf:Property ; + rdfs:label "select"@en ; + rdfs:comment "The SPARQL SELECT query to execute."@en ; + rdfs:domain sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:severity a rdf:Property ; + rdfs:label "severity"@en ; + rdfs:comment "Defines the severity that validation results produced by a shape must have. Defaults to sh:Violation."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Severity . + +sh:shapesGraph a rdf:Property ; + rdfs:label "shapes graph"@en ; + rdfs:comment "Shapes graphs that should be used when validating this data graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:shapesGraphWellFormed a rdf:Property ; + rdfs:label "shapes graph well-formed"@en ; + rdfs:comment "If true then the validation engine was certain that the shapes graph has passed all SHACL syntax requirements during the validation process."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:sourceConstraint a rdf:Property ; + rdfs:label "source constraint"@en ; + rdfs:comment "The constraint that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:sourceConstraintComponent a rdf:Property ; + rdfs:label "source constraint component"@en ; + rdfs:comment "The constraint component that is the source of the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ConstraintComponent . + +sh:sourceShape a rdf:Property ; + rdfs:label "source shape"@en ; + rdfs:comment "The shape that is was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:sparql a rdf:Property ; + rdfs:label "constraint (in SPARQL)"@en ; + rdfs:comment "Links a shape with SPARQL constraints."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:SPARQLConstraint . + +sh:subject a rdf:Property ; + rdfs:label "subject"@en ; + rdfs:comment "An expression producing the resources that shall be inferred as subjects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:suggestedShapesGraph a rdf:Property ; + rdfs:label "suggested shapes graph"@en ; + rdfs:comment "Suggested shapes graphs for this ontology. The values of this property may be used in the absence of specific sh:shapesGraph statements."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:target a rdf:Property ; + rdfs:label "target"@en ; + rdfs:comment "Links a shape to a target specified by an extension language, for example instances of sh:SPARQLTarget."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Target . + +sh:targetClass a rdf:Property ; + rdfs:label "target class"@en ; + rdfs:comment "Links a shape to a class, indicating that all instances of the class must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:targetNode a rdf:Property ; + rdfs:label "target node"@en ; + rdfs:comment "Links a shape to individual nodes, indicating that these nodes must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:targetObjectsOf a rdf:Property ; + rdfs:label "target objects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all all objects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:targetSubjectsOf a rdf:Property ; + rdfs:label "target subjects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all subjects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:union a rdf:Property ; + rdfs:label "union"@en ; + rdfs:comment "A list of node expressions that shall be used together."@en ; + rdfs:isDefinedBy sh: . + +sh:uniqueLang a rdf:Property ; + rdfs:label "unique languages"@en ; + rdfs:comment "Specifies whether all node values must have a unique (or no) language tag."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:update a rdf:Property ; + rdfs:label "update"@en ; + rdfs:comment "The SPARQL UPDATE to execute."@en ; + rdfs:domain sh:SPARQLUpdateExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:validator a rdf:Property ; + rdfs:label "validator"@en ; + rdfs:comment "The validator(s) used to evaluate constraints of either node or property shapes."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:value a rdf:Property ; + rdfs:label "value"@en ; + rdfs:comment "An RDF node that has caused the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:xone a rdf:Property ; + rdfs:label "exactly one"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to exactly one of the shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:zeroOrMorePath a rdf:Property ; + rdfs:label "zero or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or more times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:zeroOrOnePath a rdf:Property ; + rdfs:label "zero or one path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or one times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:AndConstraintComponent a sh:ConstraintComponent ; + rdfs:label "And constraint component"@en ; + rdfs:comment "A constraint component that can be used to test whether a value node conforms to all members of a provided list of shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:AndConstraintComponent-and . + +sh:AndConstraintComponent-and a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:and . + +sh:BlankNode a sh:NodeKind ; + rdfs:label "Blank node"@en ; + rdfs:comment "The node kind of all blank nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrIRI a sh:NodeKind ; + rdfs:label "Blank node or IRI"@en ; + rdfs:comment "The node kind of all blank nodes or IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrLiteral a sh:NodeKind ; + rdfs:label "Blank node or literal"@en ; + rdfs:comment "The node kind of all blank nodes or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:ClassConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Class constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is an instance of a given type."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ClassConstraintComponent-class . + +sh:ClassConstraintComponent-class a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:class . + +sh:ClosedConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Closed constraint component"@en ; + rdfs:comment "A constraint component that can be used to indicate that focus nodes must only have values for those properties that have been explicitly enumerated via sh:property/sh:path."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ClosedConstraintComponent-closed ; + sh:parameter sh:ClosedConstraintComponent-ignoredProperties . + +sh:ClosedConstraintComponent-closed a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:path sh:closed . + +sh:ClosedConstraintComponent-ignoredProperties a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:optional true ; + sh:path sh:ignoredProperties . + +sh:DatatypeConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Datatype constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the datatype of all value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:DatatypeConstraintComponent-datatype . + +sh:DatatypeConstraintComponent-datatype a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:datatype . + +sh:DisjointConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Disjoint constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is disjoint with the the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:DisjointConstraintComponent-disjoint . + +sh:DisjointConstraintComponent-disjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:disjoint . + +sh:EqualsConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is equal to the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:EqualsConstraintComponent-equals . + +sh:EqualsConstraintComponent-equals a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:equals . + +sh:ExpressionConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Expression constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a given node expression produces true for all value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ExpressionConstraintComponent-expression . + +sh:ExpressionConstraintComponent-expression a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:expression . + +sh:HasValueConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Has-value constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that one of the value nodes is a given RDF node."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:HasValueConstraintComponent-hasValue . + +sh:HasValueConstraintComponent-hasValue a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:hasValue . + +sh:IRI a sh:NodeKind ; + rdfs:label "IRI"@en ; + rdfs:comment "The node kind of all IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:IRIOrLiteral a sh:NodeKind ; + rdfs:label "IRI or literal"@en ; + rdfs:comment "The node kind of all IRIs or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:InConstraintComponent a sh:ConstraintComponent ; + rdfs:label "In constraint component"@en ; + rdfs:comment "A constraint component that can be used to exclusively enumerate the permitted value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:InConstraintComponent-in . + +sh:InConstraintComponent-in a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:path sh:in . + +sh:Info a sh:Severity ; + rdfs:label "Info"@en ; + rdfs:comment "The severity for an informational validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:JSConstraint-js a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:js . + +sh:JSConstraintComponent a sh:ConstraintComponent ; + rdfs:label "JavaScript constraint component"@en ; + rdfs:comment "A constraint component with the parameter sh:js linking to a sh:JSConstraint containing a sh:script."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:JSConstraint-js . + +sh:LanguageInConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Language-in constraint component"@en ; + rdfs:comment "A constraint component that can be used to enumerate language tags that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LanguageInConstraintComponent-languageIn . + +sh:LanguageInConstraintComponent-languageIn a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:path sh:languageIn . + +sh:LessThanConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Less-than constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LessThanConstraintComponent-lessThan . + +sh:LessThanConstraintComponent-lessThan a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:lessThan . + +sh:LessThanOrEqualsConstraintComponent a sh:ConstraintComponent ; + rdfs:label "less-than-or-equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals . + +sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:lessThanOrEquals . + +sh:Literal a sh:NodeKind ; + rdfs:label "Literal"@en ; + rdfs:comment "The node kind of all literals."@en ; + rdfs:isDefinedBy sh: . + +sh:MaxCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum number of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxCountConstraintComponent-maxCount . + +sh:MaxCountConstraintComponent-maxCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxCount . + +sh:MaxExclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum exclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxExclusiveConstraintComponent-maxExclusive . + +sh:MaxExclusiveConstraintComponent-maxExclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxExclusive . + +sh:MaxInclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum inclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxInclusiveConstraintComponent-maxInclusive . + +sh:MaxInclusiveConstraintComponent-maxInclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxInclusive . + +sh:MaxLengthConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum string length of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxLengthConstraintComponent-maxLength . + +sh:MaxLengthConstraintComponent-maxLength a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxLength . + +sh:MinCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum number of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinCountConstraintComponent-minCount . + +sh:MinCountConstraintComponent-minCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minCount . + +sh:MinExclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum exclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinExclusiveConstraintComponent-minExclusive . + +sh:MinExclusiveConstraintComponent-minExclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minExclusive . + +sh:MinInclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum inclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinInclusiveConstraintComponent-minInclusive . + +sh:MinInclusiveConstraintComponent-minInclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minInclusive . + +sh:MinLengthConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum string length of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinLengthConstraintComponent-minLength . + +sh:MinLengthConstraintComponent-minLength a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minLength . + +sh:NodeConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Node constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given node shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NodeConstraintComponent-node . + +sh:NodeConstraintComponent-node a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:node . + +sh:NodeKindConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Node-kind constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the RDF node kind of each value node."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NodeKindConstraintComponent-nodeKind . + +sh:NodeKindConstraintComponent-nodeKind a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; + sh:maxCount 1 ; + sh:path sh:nodeKind . + +sh:NotConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Not constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that value nodes do not conform to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NotConstraintComponent-not . + +sh:NotConstraintComponent-not a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:not . + +sh:OrConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Or constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to at least one out of several provided shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:OrConstraintComponent-or . + +sh:OrConstraintComponent-or a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:or . + +sh:PatternConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Pattern constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node matches a given regular expression."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:PatternConstraintComponent-flags ; + sh:parameter sh:PatternConstraintComponent-pattern . + +sh:PatternConstraintComponent-flags a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:string ; + sh:optional true ; + sh:path sh:flags . + +sh:PatternConstraintComponent-pattern a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:string ; + sh:path sh:pattern . + +sh:PropertyConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Property constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given property shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:PropertyConstraintComponent-property . + +sh:PropertyConstraintComponent-property a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:property . + +sh:QualifiedMaxCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Qualified-max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified maximum number of value nodes conforms to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint . + +sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:path sh:qualifiedMaxCount . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:qualifiedValueShape . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:optional true ; + sh:path sh:qualifiedValueShapesDisjoint . + +sh:QualifiedMinCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Qualified-min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified minimum number of value nodes conforms to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedMinCount ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint . + +sh:QualifiedMinCountConstraintComponent-qualifiedMinCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:path sh:qualifiedMinCount . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShape a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:qualifiedValueShape . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:optional true ; + sh:path sh:qualifiedValueShapesDisjoint . + +sh:SPARQLConstraintComponent a sh:ConstraintComponent ; + rdfs:label "SPARQL constraint component"@en ; + rdfs:comment "A constraint component that can be used to define constraints based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:SPARQLConstraintComponent-sparql . + +sh:SPARQLConstraintComponent-sparql a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:sparql . + +sh:UniqueLangConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Unique-languages constraint component"@en ; + rdfs:comment "A constraint component that can be used to specify that no pair of value nodes may use the same language tag."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:UniqueLangConstraintComponent-uniqueLang . + +sh:UniqueLangConstraintComponent-uniqueLang a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:uniqueLang . + +sh:Violation a sh:Severity ; + rdfs:label "Violation"@en ; + rdfs:comment "The severity for a violation validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:Warning a sh:Severity ; + rdfs:label "Warning"@en ; + rdfs:comment "The severity for a warning validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:XoneConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Exactly one constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to exactly one out of several provided shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:XoneConstraintComponent-xone . + +sh:XoneConstraintComponent-xone a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:xone . + +sh:this a rdfs:Resource ; + rdfs:label "this"@en ; + rdfs:comment "A node expression that represents the current focus node."@en ; + rdfs:isDefinedBy sh: . + + 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/v1.2.12-style01/shacl-shacl.ttl new file mode 100644 index 0000000000..b728317864 --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl-shacl.ttl @@ -0,0 +1,431 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix sh: . +@prefix shsh: . + +shsh: rdfs:label "SHACL for SHACL"@en ; + rdfs:comment "This shapes graph can be used to validate SHACL shapes graphs against a subset of the syntax rules."@en ; + sh:declare [ + sh:namespace "http://www.w3.org/ns/shacl-shacl#" ; + sh:prefix "shsh" ; + ] . + +shsh:EntailmentShape a sh:NodeShape ; + sh:nodeKind sh:IRI ; + sh:targetObjectsOf sh:entailment . + +shsh:ListNodeShape a sh:NodeShape ; + rdfs:label "List node shape"@en ; + rdfs:comment "Defines constraints on what it means for a node to be a node within a well-formed RDF list. Note that this does not check whether the rdf:rest items are also well-formed lists as this would lead to unsupported recursion."@en ; + sh:or ( [ + sh:hasValue ( ) ; + sh:property [ + sh:maxCount 0 ; + sh:path rdf:first ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path rdf:rest ; + ] ; + ] [ + sh:not [ + sh:hasValue ( ) ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path rdf:first ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path rdf:rest ; + ] ; + ] ) . + +shsh:ListShape a sh:NodeShape ; + rdfs:label "List shape"@en ; + rdfs:comment "A shape describing well-formed RDF lists. Currently does not check for non-recursion. This could be expressed using SHACL-SPARQL."@en ; + rdfs:seeAlso ; + sh:property [ + rdfs:comment "Each list member (including this node) must be have the shape shsh:ListNodeShape."@en ; + sh:hasValue ( ) ; + sh:node shsh:ListNodeShape ; + sh:path [ + sh:zeroOrMorePath rdf:rest ; + ] ; + ] . + +shsh:NodeShapeShape a sh:NodeShape ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:path ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:lessThan ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:lessThanOrEquals ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:maxCount ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:minCount ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:qualifiedValueShape ; + ] ; + sh:property [ + sh:maxCount 0 ; + sh:path sh:uniqueLang ; + ] ; + sh:targetObjectsOf sh:node . + +shsh:PathListWithAtLeast2Members a sh:NodeShape ; + sh:node shsh:ListShape ; + sh:property [ + sh:minCount 2 ; + sh:path [ + sh:oneOrMorePath rdf:rest ; + ] ; + ] . + +shsh:PathNodeShape sh:xone ( [ + sh:nodeKind sh:IRI ; + ] [ + sh:node shsh:PathListWithAtLeast2Members ; + sh:nodeKind sh:BlankNode ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:node shsh:PathListWithAtLeast2Members ; + sh:path sh:alternativePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:inversePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:zeroOrMorePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:oneOrMorePath ; + ] ; + ] [ + sh:closed true ; + sh:nodeKind sh:BlankNode ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:path sh:zeroOrOnePath ; + ] ; + ] ) . + +shsh:PathShape a sh:NodeShape ; + rdfs:label "Path shape"@en ; + rdfs:comment "A shape that can be used to validate the syntax rules of well-formed SHACL paths."@en ; + rdfs:seeAlso ; + sh:property [ + sh:node shsh:PathNodeShape ; + sh:path [ + sh:zeroOrMorePath [ + sh:alternativePath ( ( [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ( sh:alternativePath [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) sh:inversePath sh:zeroOrMorePath sh:oneOrMorePath sh:zeroOrOnePath ) ; + ] ; + ] ; + ] . + +shsh:PropertyShapeShape a sh:NodeShape ; + sh:property [ + sh:maxCount 1 ; + sh:minCount 1 ; + sh:node shsh:PathShape ; + sh:path sh:path ; + ] ; + sh:targetObjectsOf sh:property . + +shsh:ShapeShape a sh:NodeShape ; + rdfs:label "Shape shape"@en ; + rdfs:comment "A shape that can be used to validate syntax rules for other shapes."@en ; + sh:or ( [ + sh:not [ + sh:class rdfs:Class ; + sh:or ( [ + sh:class sh:NodeShape ; + ] [ + sh:class sh:PropertyShape ; + ] ) ; + ] ; + ] [ + sh:nodeKind sh:IRI ; + ] ) ; + sh:property [ + sh:nodeKind sh:IRIOrLiteral ; + sh:path sh:targetNode ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetClass ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetSubjectsOf ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:targetObjectsOf ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:severity ; + ] ; + sh:property [ + sh:or ( [ + sh:datatype xsd:string ; + ] [ + sh:datatype rdf:langString ; + ] ) ; + sh:path sh:message ; + ] ; + sh:property [ + sh:in ( true false ) ; + sh:maxCount 1 ; + sh:path sh:deactivated ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:and ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:class ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:closed ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:ignoredProperties ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path ( sh:ignoredProperties [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:datatype ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:disjoint ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:equals ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:in ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:node shsh:ListShape ; + sh:path sh:languageIn ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:path ( sh:languageIn [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:lessThan ; + ] ; + sh:property [ + sh:nodeKind sh:IRI ; + sh:path sh:lessThanOrEquals ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxExclusive ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxInclusive ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxLength ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minExclusive ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minInclusive ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minLength ; + ] ; + sh:property [ + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; + sh:maxCount 1 ; + sh:path sh:nodeKind ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:or ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path sh:pattern ; + ] ; + sh:property [ + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path sh:flags ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:qualifiedMaxCount ; + ] ; + sh:property [ + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:qualifiedMinCount ; + ] ; + sh:property [ + sh:maxCount 1 ; + sh:path sh:qualifiedValueShape ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:qualifiedValueShapesDisjoint ; + ] ; + sh:property [ + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:uniqueLang ; + ] ; + sh:property [ + sh:node shsh:ListShape ; + sh:path sh:xone ; + ] ; + sh:targetClass sh:NodeShape ; + sh:targetClass sh:PropertyShape ; + sh:targetObjectsOf sh:node ; + sh:targetObjectsOf sh:not ; + sh:targetObjectsOf sh:property ; + sh:targetObjectsOf sh:qualifiedValueShape ; + sh:targetSubjectsOf sh:and ; + sh:targetSubjectsOf sh:class ; + sh:targetSubjectsOf sh:closed ; + sh:targetSubjectsOf sh:datatype ; + sh:targetSubjectsOf sh:disjoint ; + sh:targetSubjectsOf sh:equals ; + sh:targetSubjectsOf sh:flags ; + sh:targetSubjectsOf sh:hasValue ; + sh:targetSubjectsOf sh:ignoredProperties ; + sh:targetSubjectsOf sh:in ; + sh:targetSubjectsOf sh:languageIn ; + sh:targetSubjectsOf sh:lessThan ; + sh:targetSubjectsOf sh:lessThanOrEquals ; + sh:targetSubjectsOf sh:maxCount ; + sh:targetSubjectsOf sh:maxExclusive ; + sh:targetSubjectsOf sh:maxInclusive ; + sh:targetSubjectsOf sh:maxLength ; + sh:targetSubjectsOf sh:minCount ; + sh:targetSubjectsOf sh:minExclusive ; + sh:targetSubjectsOf sh:minInclusive ; + sh:targetSubjectsOf sh:minLength ; + sh:targetSubjectsOf sh:node ; + sh:targetSubjectsOf sh:nodeKind ; + sh:targetSubjectsOf sh:not ; + sh:targetSubjectsOf sh:or ; + sh:targetSubjectsOf sh:pattern ; + sh:targetSubjectsOf sh:property ; + sh:targetSubjectsOf sh:qualifiedMaxCount ; + sh:targetSubjectsOf sh:qualifiedMinCount ; + sh:targetSubjectsOf sh:qualifiedValueShape ; + sh:targetSubjectsOf sh:qualifiedValueShapesDisjoint ; + sh:targetSubjectsOf sh:sparql ; + sh:targetSubjectsOf sh:targetClass ; + sh:targetSubjectsOf sh:targetNode ; + sh:targetSubjectsOf sh:targetObjectsOf ; + sh:targetSubjectsOf sh:targetSubjectsOf ; + sh:targetSubjectsOf sh:uniqueLang ; + sh:targetSubjectsOf sh:xone ; + sh:xone ( shsh:NodeShapeShape shsh:PropertyShapeShape ) . + +shsh:ShapesGraphShape a sh:NodeShape ; + sh:nodeKind sh:IRI ; + sh:targetObjectsOf sh:shapesGraph . + +shsh:ShapesListShape a sh:NodeShape ; + sh:property [ + sh:node shsh:ShapeShape ; + sh:path ( [ + sh:zeroOrMorePath rdf:rest ; + ] rdf:first ) ; + ] ; + sh:targetObjectsOf sh:and ; + sh:targetObjectsOf sh:or ; + sh:targetObjectsOf sh:xone . + diff --git a/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl.ttl b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl.ttl new file mode 100644 index 0000000000..970975166a --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/expected/v1.2.12-style01/shacl.ttl @@ -0,0 +1,1346 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix owl: . +@prefix sh: . + +sh: a owl:Ontology ; + rdfs:label "W3C Shapes Constraint Language (SHACL) Vocabulary"@en ; + rdfs:comment "This vocabulary defines terms used in SHACL, the W3C Shapes Constraint Language."@en ; + sh:declare [ + sh:namespace "http://www.w3.org/ns/shacl#" ; + sh:prefix "sh" ; + ] ; + sh:suggestedShapesGraph . + +sh:AbstractResult a rdfs:Class ; + rdfs:label "Abstract result"@en ; + rdfs:comment "The base class of validation results, typically not instantiated directly."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:ConstraintComponent a rdfs:Class ; + rdfs:label "Constraint component"@en ; + rdfs:comment "The class of constraint components."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Parameterizable . + +sh:Function a rdfs:Class ; + rdfs:label "Function"@en ; + rdfs:comment "The class of SHACL functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Parameterizable . + +sh:JSConstraint a rdfs:Class ; + rdfs:label "JavaScript-based constraint"@en ; + rdfs:comment "The class of constraints backed by a JavaScript function."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable . + +sh:JSExecutable a rdfs:Class ; + rdfs:label "JavaScript executable"@en ; + rdfs:comment "Abstract base class of resources that declare an executable JavaScript."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:JSFunction a rdfs:Class ; + rdfs:label "JavaScript function"@en ; + rdfs:comment "The class of SHACL functions that execute a JavaScript function when called."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:JSExecutable . + +sh:JSLibrary a rdfs:Class ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Represents a JavaScript library, typically identified by one or more URLs of files to include."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:JSRule a rdfs:Class ; + rdfs:label "JavaScript rule"@en ; + rdfs:comment "The class of SHACL rules expressed using JavaScript."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Rule . + +sh:JSTarget a rdfs:Class ; + rdfs:label "JavaScript target"@en ; + rdfs:comment "The class of targets that are based on JavaScript functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Target . + +sh:JSTargetType a rdfs:Class ; + rdfs:label "JavaScript target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on JavaScript functions."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:TargetType . + +sh:JSValidator a rdfs:Class ; + rdfs:label "JavaScript validator"@en ; + rdfs:comment "A SHACL validator based on JavaScript. This can be used to declare SHACL constraint components that perform JavaScript-based validation when used."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Validator . + +sh:NodeKind a rdfs:Class ; + rdfs:label "Node kind"@en ; + rdfs:comment "The class of all node kinds, including sh:BlankNode, sh:IRI, sh:Literal or the combinations of these: sh:BlankNodeOrIRI, sh:BlankNodeOrLiteral, sh:IRIOrLiteral."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:NodeShape a rdfs:Class ; + rdfs:label "Node shape"@en ; + rdfs:comment "A node shape is a shape that specifies constraint that need to be met with respect to focus nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Shape . + +sh:Parameter a rdfs:Class ; + rdfs:label "Parameter"@en ; + rdfs:comment "The class of parameter declarations, consisting of a path predicate and (possibly) information about allowed value type, cardinality and other characteristics."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:PropertyShape . + +sh:Parameterizable a rdfs:Class ; + rdfs:label "Parameterizable"@en ; + rdfs:comment "Superclass of components that can take parameters, especially functions and constraint components."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PrefixDeclaration a rdfs:Class ; + rdfs:label "Prefix declaration"@en ; + rdfs:comment "The class of prefix declarations, consisting of pairs of a prefix with a namespace."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PropertyGroup a rdfs:Class ; + rdfs:label "Property group"@en ; + rdfs:comment "Instances of this class represent groups of property shapes that belong together."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:PropertyShape a rdfs:Class ; + rdfs:label "Property shape"@en ; + rdfs:comment "A property shape is a shape that specifies constraints on the values of a focus node for a given property or path."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Shape . + +sh:ResultAnnotation a rdfs:Class ; + rdfs:label "Result annotation"@en ; + rdfs:comment "A class of result annotations, which define the rules to derive the values of a given annotation property as extra values for a validation result."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Rule a rdfs:Class ; + rdfs:label "Rule"@en ; + rdfs:comment "The class of SHACL rules. Never instantiated directly."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:SPARQLAskExecutable a rdfs:Class ; + rdfs:label "SPARQL ASK executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on an ASK query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLAskValidator a rdfs:Class ; + rdfs:label "SPARQL ASK validator"@en ; + rdfs:comment "The class of validators based on SPARQL ASK queries. The queries are evaluated for each value node and are supposed to return true if the given node conforms."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:Validator . + +sh:SPARQLConstraint a rdfs:Class ; + rdfs:label "SPARQL constraint"@en ; + rdfs:comment "The class of constraints based on SPARQL SELECT queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLSelectExecutable . + +sh:SPARQLConstructExecutable a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on a CONSTRUCT query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLExecutable a rdfs:Class ; + rdfs:label "SPARQL executable"@en ; + rdfs:comment "The class of resources that encapsulate a SPARQL query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:SPARQLFunction a rdfs:Class ; + rdfs:label "SPARQL function"@en ; + rdfs:comment "A function backed by a SPARQL query - either ASK or SELECT."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable . + +sh:SPARQLRule a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT rule"@en ; + rdfs:comment "The class of SHACL rules based on SPARQL CONSTRUCT queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Rule ; + rdfs:subClassOf sh:SPARQLConstructExecutable . + +sh:SPARQLSelectExecutable a rdfs:Class ; + rdfs:label "SPARQL SELECT executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SELECT query."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:SPARQLSelectValidator a rdfs:Class ; + rdfs:label "SPARQL SELECT validator"@en ; + rdfs:comment "The class of validators based on SPARQL SELECT queries. The queries are evaluated for each focus node and are supposed to produce bindings for all focus nodes that do not conform."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:Validator . + +sh:SPARQLTarget a rdfs:Class ; + rdfs:label "SPARQL target"@en ; + rdfs:comment "The class of targets that are based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:Target . + +sh:SPARQLTargetType a rdfs:Class ; + rdfs:label "SPARQL target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:subClassOf sh:TargetType . + +sh:SPARQLUpdateExecutable a rdfs:Class ; + rdfs:label "SPARQL UPDATE executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SPARQL UPDATE."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:SPARQLExecutable . + +sh:Severity a rdfs:Class ; + rdfs:label "Severity"@en ; + rdfs:comment "The class of validation result severity levels, including violation and warning levels."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Shape a rdfs:Class ; + rdfs:label "Shape"@en ; + rdfs:comment "A shape is a collection of constraints that may be targeted for certain nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:Target a rdfs:Class ; + rdfs:label "Target"@en ; + rdfs:comment "The base class of targets such as those based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:TargetType a rdfs:Class ; + rdfs:label "Target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets.\tInstances of this are instantiated as values of the sh:target property."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Class ; + rdfs:subClassOf sh:Parameterizable . + +sh:TripleRule a rdfs:Class ; + rdfs:label "A rule based on triple (subject, predicate, object) pattern."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:Rule . + +sh:ValidationReport a rdfs:Class ; + rdfs:label "Validation report"@en ; + rdfs:comment "The class of SHACL validation reports."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:ValidationResult a rdfs:Class ; + rdfs:label "Validation result"@en ; + rdfs:comment "The class of validation results."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf sh:AbstractResult . + +sh:Validator a rdfs:Class ; + rdfs:label "Validator"@en ; + rdfs:comment "The class of validators, which provide instructions on how to process a constraint definition. This class serves as base class for the SPARQL-based validators and other possible implementations."@en ; + rdfs:isDefinedBy sh: ; + rdfs:subClassOf rdfs:Resource . + +sh:alternativePath a rdf:Property ; + rdfs:label "alternative path"@en ; + rdfs:comment "The (single) value of this property must be a list of path elements, representing the elements of alternative paths."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:and a rdf:Property ; + rdfs:label "and"@en ; + rdfs:comment "RDF list of shapes to validate the value nodes against."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:annotationProperty a rdf:Property ; + rdfs:label "annotation property"@en ; + rdfs:comment "The annotation property that shall be set."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:annotationValue a rdf:Property ; + rdfs:label "annotation value"@en ; + rdfs:comment "The (default) values of the annotation property."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: . + +sh:annotationVarName a rdf:Property ; + rdfs:label "annotation variable name"@en ; + rdfs:comment "The name of the SPARQL variable from the SELECT clause that shall be used for the values."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:ask a rdf:Property ; + rdfs:label "ask"@en ; + rdfs:comment "The SPARQL ASK query to execute."@en ; + rdfs:domain sh:SPARQLAskExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:class a rdf:Property ; + rdfs:label "class"@en ; + rdfs:comment "The type that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:closed a rdf:Property ; + rdfs:label "closed"@en ; + rdfs:comment "If set to true then the shape is closed."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:condition a rdf:Property ; + rdfs:label "condition"@en ; + rdfs:comment "The shapes that the focus nodes need to conform to before a rule is executed on them."@en ; + rdfs:domain sh:Rule ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:conforms a rdf:Property ; + rdfs:label "conforms"@en ; + rdfs:comment "True if the validation did not produce any validation results, and false otherwise."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:construct a rdf:Property ; + rdfs:label "construct"@en ; + rdfs:comment "The SPARQL CONSTRUCT query to execute."@en ; + rdfs:domain sh:SPARQLConstructExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:datatype a rdf:Property ; + rdfs:label "datatype"@en ; + rdfs:comment "Specifies an RDF datatype that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Datatype . + +sh:deactivated a rdf:Property ; + rdfs:label "deactivated"@en ; + rdfs:comment "If set to true then all nodes conform to this."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:declare a rdf:Property ; + rdfs:label "declare"@en ; + rdfs:comment "Links a resource with its namespace prefix declarations."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PrefixDeclaration . + +sh:defaultValue a rdf:Property ; + rdfs:label "default value"@en ; + rdfs:comment "A default value for a property, for example for user interface tools to pre-populate input fields."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:description a rdf:Property ; + rdfs:label "description"@en ; + rdfs:comment "Human-readable descriptions for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:detail a rdf:Property ; + rdfs:label "detail"@en ; + rdfs:comment "Links a result with other results that provide more details, for example to describe violations against nested shapes."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:AbstractResult . + +sh:disjoint a rdf:Property ; + rdfs:label "disjoint"@en ; + rdfs:comment "Specifies a property where the set of values must be disjoint with the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:entailment a rdf:Property ; + rdfs:label "entailment"@en ; + rdfs:comment "An entailment regime that indicates what kind of inferencing is required by a shapes graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:equals a rdf:Property ; + rdfs:label "equals"@en ; + rdfs:comment "Specifies a property that must have the same values as the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:expression a rdf:Property ; + rdfs:label "expression"@en ; + rdfs:comment "The node expression that must return true for the value nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:filterShape a rdf:Property ; + rdfs:label "filter shape"@en ; + rdfs:comment "The shape that all input nodes of the expression need to conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:flags a rdf:Property ; + rdfs:label "flags"@en ; + rdfs:comment "An optional flag to be used with regular expression pattern matching."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:focusNode a rdf:Property ; + rdfs:label "focus node"@en ; + rdfs:comment "The focus node that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:group a rdf:Property ; + rdfs:label "group"@en ; + rdfs:comment "Can be used to link to a property group to indicate that a property shape belongs to a group of related property shapes."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PropertyGroup . + +sh:hasValue a rdf:Property ; + rdfs:label "has value"@en ; + rdfs:comment "Specifies a value that must be among the value nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:ignoredProperties a rdf:Property ; + rdfs:label "ignored properties"@en ; + rdfs:comment "An optional RDF list of properties that are also permitted in addition to those explicitly enumerated via sh:property/sh:path."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:in a rdf:Property ; + rdfs:label "in"@en ; + rdfs:comment "Specifies a list of allowed values so that each value node must be among the members of the given list."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:intersection a rdf:Property ; + rdfs:label "intersection"@en ; + rdfs:comment "A list of node expressions that shall be intersected."@en ; + rdfs:isDefinedBy sh: . + +sh:inversePath a rdf:Property ; + rdfs:label "inverse path"@en ; + rdfs:comment "The (single) value of this property represents an inverse path (object to subject)."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:js a rdf:Property ; + rdfs:label "JavaScript constraint"@en ; + rdfs:comment "Constraints expressed in JavaScript." ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:JSConstraint . + +sh:jsFunctionName a rdf:Property ; + rdfs:label "JavaScript function name"@en ; + rdfs:comment "The name of the JavaScript function to execute."@en ; + rdfs:domain sh:JSExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:jsLibrary a rdf:Property ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Declares which JavaScript libraries are needed to execute this."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:JSLibrary . + +sh:jsLibraryURL a rdf:Property ; + rdfs:label "JavaScript library URL"@en ; + rdfs:comment "Declares the URLs of a JavaScript library. This should be the absolute URL of a JavaScript file. Implementations may redirect those to local files."@en ; + rdfs:domain sh:JSLibrary ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:anyURI . + +sh:labelTemplate a rdf:Property ; + rdfs:label "label template"@en ; + rdfs:comment "Outlines how human-readable labels of instances of the associated Parameterizable shall be produced. The values can contain {?paramName} as placeholders for the actual values of the given parameter."@en ; + rdfs:domain sh:Parameterizable ; + rdfs:isDefinedBy sh: . + +sh:languageIn a rdf:Property ; + rdfs:label "language in"@en ; + rdfs:comment "Specifies a list of language tags that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:lessThan a rdf:Property ; + rdfs:label "less than"@en ; + rdfs:comment "Specifies a property that must have smaller values than the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:lessThanOrEquals a rdf:Property ; + rdfs:label "less than or equals"@en ; + rdfs:comment "Specifies a property that must have smaller or equal values than the value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:maxCount a rdf:Property ; + rdfs:label "max count"@en ; + rdfs:comment "Specifies the maximum number of values in the set of value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:maxExclusive a rdf:Property ; + rdfs:label "max exclusive"@en ; + rdfs:comment "Specifies the maximum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:maxInclusive a rdf:Property ; + rdfs:label "max inclusive"@en ; + rdfs:comment "Specifies the maximum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:maxLength a rdf:Property ; + rdfs:label "max length"@en ; + rdfs:comment "Specifies the maximum string length of each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:message a rdf:Property ; + rdfs:label "message"@en ; + rdfs:comment "A human-readable message (possibly with placeholders for variables) explaining the cause of the result."@en ; + rdfs:isDefinedBy sh: . + +sh:minCount a rdf:Property ; + rdfs:label "min count"@en ; + rdfs:comment "Specifies the minimum number of values in the set of value nodes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:minExclusive a rdf:Property ; + rdfs:label "min exclusive"@en ; + rdfs:comment "Specifies the minimum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:minInclusive a rdf:Property ; + rdfs:label "min inclusive"@en ; + rdfs:comment "Specifies the minimum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + +sh:minLength a rdf:Property ; + rdfs:label "min length"@en ; + rdfs:comment "Specifies the minimum string length of each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:name a rdf:Property ; + rdfs:label "name"@en ; + rdfs:comment "Human-readable labels for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:namespace a rdf:Property ; + rdfs:label "namespace"@en ; + rdfs:comment "The namespace associated with a prefix in a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:anyURI . + +sh:node a rdf:Property ; + rdfs:label "node"@en ; + rdfs:comment "Specifies the node shape that all value nodes must conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:NodeShape . + +sh:nodeKind a rdf:Property ; + rdfs:label "node kind"@en ; + rdfs:comment "Specifies the node kind (e.g. IRI or literal) each value node."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:NodeKind . + +sh:nodeValidator a rdf:Property ; + rdfs:label "shape validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a node shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:nodes a rdf:Property ; + rdfs:label "nodes"@en ; + rdfs:comment "The node expression producing the input nodes of a filter shape expression."@en ; + rdfs:isDefinedBy sh: . + +sh:not a rdf:Property ; + rdfs:label "not"@en ; + rdfs:comment "Specifies a shape that the value nodes must not conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:object a rdf:Property ; + rdfs:label "object"@en ; + rdfs:comment "An expression producing the nodes that shall be inferred as objects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:oneOrMorePath a rdf:Property ; + rdfs:label "one or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched one or more times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:optional a rdf:Property ; + rdfs:label "optional"@en ; + rdfs:comment "Indicates whether a parameter is optional."@en ; + rdfs:domain sh:Parameter ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:or a rdf:Property ; + rdfs:label "or"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to at least one of the shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:order a rdf:Property ; + rdfs:label "order"@en ; + rdfs:comment "Specifies the relative order of this compared to its siblings. For example use 0 for the first, 1 for the second."@en ; + rdfs:isDefinedBy sh: . + +sh:parameter a rdf:Property ; + rdfs:label "parameter"@en ; + rdfs:comment "The parameters of a function or constraint component."@en ; + rdfs:domain sh:Parameterizable ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Parameter . + +sh:path a rdf:Property ; + rdfs:label "path"@en ; + rdfs:comment "Specifies the property path of a property shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:pattern a rdf:Property ; + rdfs:label "pattern"@en ; + rdfs:comment "Specifies a regular expression pattern that the string representations of the value nodes must match."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:predicate a rdf:Property ; + rdfs:label "predicate"@en ; + rdfs:comment "An expression producing the properties that shall be inferred as predicates."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:prefix a rdf:Property ; + rdfs:label "prefix"@en ; + rdfs:comment "The prefix of a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:prefixes a rdf:Property ; + rdfs:label "prefixes"@en ; + rdfs:comment "The prefixes that shall be applied before parsing the associated SPARQL query."@en ; + rdfs:domain sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:property a rdf:Property ; + rdfs:label "property"@en ; + rdfs:comment "Links a shape to its property shapes."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:PropertyShape . + +sh:propertyValidator a rdf:Property ; + rdfs:label "property validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a property shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:qualifiedMaxCount a rdf:Property ; + rdfs:label "qualified max count"@en ; + rdfs:comment "The maximum number of value nodes that can conform to the shape."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:qualifiedMinCount a rdf:Property ; + rdfs:label "qualified min count"@en ; + rdfs:comment "The minimum number of value nodes that must conform to the shape."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:integer . + +sh:qualifiedValueShape a rdf:Property ; + rdfs:label "qualified value shape"@en ; + rdfs:comment "The shape that a specified number of values must conform to."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:qualifiedValueShapesDisjoint a rdf:Property ; + rdfs:label "qualified value shapes disjoint"@en ; + rdfs:comment "Can be used to mark the qualified value shape to be disjoint with its sibling shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:result a rdf:Property ; + rdfs:label "result"@en ; + rdfs:comment "The validation results contained in a validation report."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ValidationResult . + +sh:resultAnnotation a rdf:Property ; + rdfs:label "result annotation"@en ; + rdfs:comment "Links a SPARQL validator with zero or more sh:ResultAnnotation instances, defining how to derive additional result properties based on the variables of the SELECT query."@en ; + rdfs:domain sh:SPARQLSelectValidator ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ResultAnnotation . + +sh:resultMessage a rdf:Property ; + rdfs:label "result message"@en ; + rdfs:comment "Human-readable messages explaining the cause of the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:resultPath a rdf:Property ; + rdfs:label "result path"@en ; + rdfs:comment "The path of a validation result, based on the path of the validated property shape."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:resultSeverity a rdf:Property ; + rdfs:label "result severity"@en ; + rdfs:comment "The severity of the result, e.g. warning."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Severity . + +sh:returnType a rdf:Property ; + rdfs:label "return type"@en ; + rdfs:comment "The expected type of values returned by the associated function."@en ; + rdfs:domain sh:Function ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:rule a rdf:Property ; + rdfs:label "rule"@en ; + rdfs:comment "The rules linked to a shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Rule . + +sh:select a rdf:Property ; + rdfs:label "select"@en ; + rdfs:comment "The SPARQL SELECT query to execute."@en ; + rdfs:domain sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:severity a rdf:Property ; + rdfs:label "severity"@en ; + rdfs:comment "Defines the severity that validation results produced by a shape must have. Defaults to sh:Violation."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Severity . + +sh:shapesGraph a rdf:Property ; + rdfs:label "shapes graph"@en ; + rdfs:comment "Shapes graphs that should be used when validating this data graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:shapesGraphWellFormed a rdf:Property ; + rdfs:label "shapes graph well-formed"@en ; + rdfs:comment "If true then the validation engine was certain that the shapes graph has passed all SHACL syntax requirements during the validation process."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:sourceConstraint a rdf:Property ; + rdfs:label "source constraint"@en ; + rdfs:comment "The constraint that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:sourceConstraintComponent a rdf:Property ; + rdfs:label "source constraint component"@en ; + rdfs:comment "The constraint component that is the source of the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:ConstraintComponent . + +sh:sourceShape a rdf:Property ; + rdfs:label "source shape"@en ; + rdfs:comment "The shape that is was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Shape . + +sh:sparql a rdf:Property ; + rdfs:label "constraint (in SPARQL)"@en ; + rdfs:comment "Links a shape with SPARQL constraints."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:SPARQLConstraint . + +sh:subject a rdf:Property ; + rdfs:label "subject"@en ; + rdfs:comment "An expression producing the resources that shall be inferred as subjects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:suggestedShapesGraph a rdf:Property ; + rdfs:label "suggested shapes graph"@en ; + rdfs:comment "Suggested shapes graphs for this ontology. The values of this property may be used in the absence of specific sh:shapesGraph statements."@en ; + rdfs:domain owl:Ontology ; + rdfs:isDefinedBy sh: ; + rdfs:range owl:Ontology . + +sh:target a rdf:Property ; + rdfs:label "target"@en ; + rdfs:comment "Links a shape to a target specified by an extension language, for example instances of sh:SPARQLTarget."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Target . + +sh:targetClass a rdf:Property ; + rdfs:label "target class"@en ; + rdfs:comment "Links a shape to a class, indicating that all instances of the class must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Class . + +sh:targetNode a rdf:Property ; + rdfs:label "target node"@en ; + rdfs:comment "Links a shape to individual nodes, indicating that these nodes must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:targetObjectsOf a rdf:Property ; + rdfs:label "target objects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all all objects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:targetSubjectsOf a rdf:Property ; + rdfs:label "target subjects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all subjects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:Property . + +sh:union a rdf:Property ; + rdfs:label "union"@en ; + rdfs:comment "A list of node expressions that shall be used together."@en ; + rdfs:isDefinedBy sh: . + +sh:uniqueLang a rdf:Property ; + rdfs:label "unique languages"@en ; + rdfs:comment "Specifies whether all node values must have a unique (or no) language tag."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:boolean . + +sh:update a rdf:Property ; + rdfs:label "update"@en ; + rdfs:comment "The SPARQL UPDATE to execute."@en ; + rdfs:domain sh:SPARQLUpdateExecutable ; + rdfs:isDefinedBy sh: ; + rdfs:range xsd:string . + +sh:validator a rdf:Property ; + rdfs:label "validator"@en ; + rdfs:comment "The validator(s) used to evaluate constraints of either node or property shapes."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:isDefinedBy sh: ; + rdfs:range sh:Validator . + +sh:value a rdf:Property ; + rdfs:label "value"@en ; + rdfs:comment "An RDF node that has caused the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:xone a rdf:Property ; + rdfs:label "exactly one"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to exactly one of the shapes."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdf:List . + +sh:zeroOrMorePath a rdf:Property ; + rdfs:label "zero or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or more times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:zeroOrOnePath a rdf:Property ; + rdfs:label "zero or one path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or one times."@en ; + rdfs:isDefinedBy sh: ; + rdfs:range rdfs:Resource . + +sh:AndConstraintComponent a sh:ConstraintComponent ; + rdfs:label "And constraint component"@en ; + rdfs:comment "A constraint component that can be used to test whether a value node conforms to all members of a provided list of shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:AndConstraintComponent-and . + +sh:AndConstraintComponent-and a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:and . + +sh:BlankNode a sh:NodeKind ; + rdfs:label "Blank node"@en ; + rdfs:comment "The node kind of all blank nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrIRI a sh:NodeKind ; + rdfs:label "Blank node or IRI"@en ; + rdfs:comment "The node kind of all blank nodes or IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrLiteral a sh:NodeKind ; + rdfs:label "Blank node or literal"@en ; + rdfs:comment "The node kind of all blank nodes or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:ClassConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Class constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is an instance of a given type."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ClassConstraintComponent-class . + +sh:ClassConstraintComponent-class a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:class . + +sh:ClosedConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Closed constraint component"@en ; + rdfs:comment "A constraint component that can be used to indicate that focus nodes must only have values for those properties that have been explicitly enumerated via sh:property/sh:path."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ClosedConstraintComponent-closed ; + sh:parameter sh:ClosedConstraintComponent-ignoredProperties . + +sh:ClosedConstraintComponent-closed a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:path sh:closed . + +sh:ClosedConstraintComponent-ignoredProperties a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:optional true ; + sh:path sh:ignoredProperties . + +sh:DatatypeConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Datatype constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the datatype of all value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:DatatypeConstraintComponent-datatype . + +sh:DatatypeConstraintComponent-datatype a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:IRI ; + sh:path sh:datatype . + +sh:DisjointConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Disjoint constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is disjoint with the the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:DisjointConstraintComponent-disjoint . + +sh:DisjointConstraintComponent-disjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:disjoint . + +sh:EqualsConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is equal to the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:EqualsConstraintComponent-equals . + +sh:EqualsConstraintComponent-equals a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:equals . + +sh:ExpressionConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Expression constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a given node expression produces true for all value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:ExpressionConstraintComponent-expression . + +sh:ExpressionConstraintComponent-expression a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:expression . + +sh:HasValueConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Has-value constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that one of the value nodes is a given RDF node."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:HasValueConstraintComponent-hasValue . + +sh:HasValueConstraintComponent-hasValue a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:hasValue . + +sh:IRI a sh:NodeKind ; + rdfs:label "IRI"@en ; + rdfs:comment "The node kind of all IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:IRIOrLiteral a sh:NodeKind ; + rdfs:label "IRI or literal"@en ; + rdfs:comment "The node kind of all IRIs or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:InConstraintComponent a sh:ConstraintComponent ; + rdfs:label "In constraint component"@en ; + rdfs:comment "A constraint component that can be used to exclusively enumerate the permitted value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:InConstraintComponent-in . + +sh:InConstraintComponent-in a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:path sh:in . + +sh:Info a sh:Severity ; + rdfs:label "Info"@en ; + rdfs:comment "The severity for an informational validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:JSConstraint-js a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:js . + +sh:JSConstraintComponent a sh:ConstraintComponent ; + rdfs:label "JavaScript constraint component"@en ; + rdfs:comment "A constraint component with the parameter sh:js linking to a sh:JSConstraint containing a sh:script."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:JSConstraint-js . + +sh:LanguageInConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Language-in constraint component"@en ; + rdfs:comment "A constraint component that can be used to enumerate language tags that all value nodes must have."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LanguageInConstraintComponent-languageIn . + +sh:LanguageInConstraintComponent-languageIn a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:path sh:languageIn . + +sh:LessThanConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Less-than constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LessThanConstraintComponent-lessThan . + +sh:LessThanConstraintComponent-lessThan a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:lessThan . + +sh:LessThanOrEqualsConstraintComponent a sh:ConstraintComponent ; + rdfs:label "less-than-or-equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals . + +sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:nodeKind sh:IRI ; + sh:path sh:lessThanOrEquals . + +sh:Literal a sh:NodeKind ; + rdfs:label "Literal"@en ; + rdfs:comment "The node kind of all literals."@en ; + rdfs:isDefinedBy sh: . + +sh:MaxCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum number of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxCountConstraintComponent-maxCount . + +sh:MaxCountConstraintComponent-maxCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxCount . + +sh:MaxExclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum exclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxExclusiveConstraintComponent-maxExclusive . + +sh:MaxExclusiveConstraintComponent-maxExclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxExclusive . + +sh:MaxInclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum inclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxInclusiveConstraintComponent-maxInclusive . + +sh:MaxInclusiveConstraintComponent-maxInclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:maxInclusive . + +sh:MaxLengthConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Max-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum string length of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MaxLengthConstraintComponent-maxLength . + +sh:MaxLengthConstraintComponent-maxLength a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:maxLength . + +sh:MinCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum number of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinCountConstraintComponent-minCount . + +sh:MinCountConstraintComponent-minCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minCount . + +sh:MinExclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum exclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinExclusiveConstraintComponent-minExclusive . + +sh:MinExclusiveConstraintComponent-minExclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minExclusive . + +sh:MinInclusiveConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum inclusive value."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinInclusiveConstraintComponent-minInclusive . + +sh:MinInclusiveConstraintComponent-minInclusive a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + sh:path sh:minInclusive . + +sh:MinLengthConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Min-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum string length of value nodes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:MinLengthConstraintComponent-minLength . + +sh:MinLengthConstraintComponent-minLength a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path sh:minLength . + +sh:NodeConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Node constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given node shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NodeConstraintComponent-node . + +sh:NodeConstraintComponent-node a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:node . + +sh:NodeKindConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Node-kind constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the RDF node kind of each value node."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NodeKindConstraintComponent-nodeKind . + +sh:NodeKindConstraintComponent-nodeKind a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI + sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; + sh:maxCount 1 ; + sh:path sh:nodeKind . + +sh:NotConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Not constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that value nodes do not conform to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:NotConstraintComponent-not . + +sh:NotConstraintComponent-not a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:not . + +sh:OrConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Or constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to at least one out of several provided shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:OrConstraintComponent-or . + +sh:OrConstraintComponent-or a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:or . + +sh:PatternConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Pattern constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node matches a given regular expression."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:PatternConstraintComponent-flags ; + sh:parameter sh:PatternConstraintComponent-pattern . + +sh:PatternConstraintComponent-flags a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:string ; + sh:optional true ; + sh:path sh:flags . + +sh:PatternConstraintComponent-pattern a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:string ; + sh:path sh:pattern . + +sh:PropertyConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Property constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given property shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:PropertyConstraintComponent-property . + +sh:PropertyConstraintComponent-property a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:property . + +sh:QualifiedMaxCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Qualified-max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified maximum number of value nodes conforms to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint . + +sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:path sh:qualifiedMaxCount . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:qualifiedValueShape . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:optional true ; + sh:path sh:qualifiedValueShapesDisjoint . + +sh:QualifiedMinCountConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Qualified-min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified minimum number of value nodes conforms to a given shape."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedMinCount ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint . + +sh:QualifiedMinCountConstraintComponent-qualifiedMinCount a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:integer ; + sh:path sh:qualifiedMinCount . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShape a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:qualifiedValueShape . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:optional true ; + sh:path sh:qualifiedValueShapesDisjoint . + +sh:SPARQLConstraintComponent a sh:ConstraintComponent ; + rdfs:label "SPARQL constraint component"@en ; + rdfs:comment "A constraint component that can be used to define constraints based on SPARQL queries."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:SPARQLConstraintComponent-sparql . + +sh:SPARQLConstraintComponent-sparql a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:sparql . + +sh:UniqueLangConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Unique-languages constraint component"@en ; + rdfs:comment "A constraint component that can be used to specify that no pair of value nodes may use the same language tag."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:UniqueLangConstraintComponent-uniqueLang . + +sh:UniqueLangConstraintComponent-uniqueLang a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + sh:path sh:uniqueLang . + +sh:Violation a sh:Severity ; + rdfs:label "Violation"@en ; + rdfs:comment "The severity for a violation validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:Warning a sh:Severity ; + rdfs:label "Warning"@en ; + rdfs:comment "The severity for a warning validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:XoneConstraintComponent a sh:ConstraintComponent ; + rdfs:label "Exactly one constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to exactly one out of several provided shapes."@en ; + rdfs:isDefinedBy sh: ; + sh:parameter sh:XoneConstraintComponent-xone . + +sh:XoneConstraintComponent-xone a sh:Parameter ; + rdfs:isDefinedBy sh: ; + sh:path sh:xone . + +sh:this a rdfs:Resource ; + rdfs:label "this"@en ; + rdfs:comment "A node expression that represents the current focus node."@en ; + rdfs:isDefinedBy sh: . + diff --git a/testlib/src/main/resources/rdf/ttl/input/shacl-shacl.ttl b/testlib/src/main/resources/rdf/ttl/input/shacl-shacl.ttl new file mode 100644 index 0000000000..26e18590cc --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/input/shacl-shacl.ttl @@ -0,0 +1,410 @@ +# baseURI: http://www.w3.org/ns/shacl-shacl# + +# A SHACL shapes graph to validate SHACL shapes graphs +# Draft last edited 2017-04-04 + +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . + +@prefix shsh: . + +shsh: + rdfs:label "SHACL for SHACL"@en ; + rdfs:comment "This shapes graph can be used to validate SHACL shapes graphs against a subset of the syntax rules."@en ; + sh:declare [ + sh:prefix "shsh" ; + sh:namespace "http://www.w3.org/ns/shacl-shacl#" ; + ] . + + +shsh:ListShape + a sh:NodeShape ; + rdfs:label "List shape"@en ; + rdfs:comment "A shape describing well-formed RDF lists. Currently does not check for non-recursion. This could be expressed using SHACL-SPARQL."@en ; + rdfs:seeAlso ; + sh:property [ + sh:path [ sh:zeroOrMorePath rdf:rest ] ; + rdfs:comment "Each list member (including this node) must be have the shape shsh:ListNodeShape."@en ; + sh:hasValue rdf:nil ; + sh:node shsh:ListNodeShape ; + ] . + +shsh:ListNodeShape + a sh:NodeShape ; + rdfs:label "List node shape"@en ; + rdfs:comment "Defines constraints on what it means for a node to be a node within a well-formed RDF list. Note that this does not check whether the rdf:rest items are also well-formed lists as this would lead to unsupported recursion."@en ; + sh:or ( [ + sh:hasValue rdf:nil ; + sh:property [ + sh:path rdf:first ; + sh:maxCount 0 ; + ] ; + sh:property [ + sh:path rdf:rest ; + sh:maxCount 0 ; + ] ; + ] + [ + sh:not [ sh:hasValue rdf:nil ] ; + sh:property [ + sh:path rdf:first ; + sh:maxCount 1 ; + sh:minCount 1 ; + ] ; + sh:property [ + sh:path rdf:rest ; + sh:maxCount 1 ; + sh:minCount 1 ; + ] ; + ] ) . + +shsh:ShapeShape + a sh:NodeShape ; + rdfs:label "Shape shape"@en ; + rdfs:comment "A shape that can be used to validate syntax rules for other shapes."@en ; + + # See https://www.w3.org/TR/shacl/#shapes for what counts as a shape + sh:targetClass sh:NodeShape ; + sh:targetClass sh:PropertyShape ; + sh:targetSubjectsOf sh:targetClass, sh:targetNode, sh:targetObjectsOf, sh:targetSubjectsOf ; + sh:targetSubjectsOf sh:and, sh:class, sh:closed, sh:datatype, sh:disjoint, sh:equals, sh:flags, sh:hasValue, + sh:ignoredProperties, sh:in, sh:languageIn, sh:lessThan, sh:lessThanOrEquals, sh:maxCount, sh:maxExclusive, + sh:maxInclusive, sh:maxLength, sh:minCount, sh:minExclusive, sh:minInclusive, sh:minLength, sh:node, sh:nodeKind, + sh:not, sh:or, sh:pattern, sh:property, sh:qualifiedMaxCount, sh:qualifiedMinCount, sh:qualifiedValueShape, + sh:qualifiedValueShape, sh:qualifiedValueShapesDisjoint, sh:qualifiedValueShapesDisjoint, sh:sparql, sh:uniqueLang, sh:xone ; + + sh:targetObjectsOf sh:node ; # node-node + sh:targetObjectsOf sh:not ; # not-node + sh:targetObjectsOf sh:property ; # property-node + sh:targetObjectsOf sh:qualifiedValueShape ; # qualifiedValueShape-node + + # Shapes are either node shapes or property shapes + sh:xone ( shsh:NodeShapeShape shsh:PropertyShapeShape ) ; + + sh:property [ + sh:path sh:targetNode ; + sh:nodeKind sh:IRIOrLiteral ; # targetNode-nodeKind + ] ; + sh:property [ + sh:path sh:targetClass ; + sh:nodeKind sh:IRI ; # targetClass-nodeKind + ] ; + sh:property [ + sh:path sh:targetSubjectsOf ; + sh:nodeKind sh:IRI ; # targetSubjectsOf-nodeKind + ] ; + sh:property [ + sh:path sh:targetObjectsOf ; + sh:nodeKind sh:IRI ; # targetObjectsOf-nodeKind + ] ; + sh:or ( [ sh:not [ + sh:class rdfs:Class ; + sh:or ( [ sh:class sh:NodeShape ] [ sh:class sh:PropertyShape ] ) + ] ] + [ sh:nodeKind sh:IRI ] + ) ; # implicit-targetClass-nodeKind + + sh:property [ + sh:path sh:severity ; + sh:maxCount 1 ; # severity-maxCount + sh:nodeKind sh:IRI ; # severity-nodeKind + ] ; + sh:property [ + sh:path sh:message ; + sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ; # message-datatype + ] ; + sh:property [ + sh:path sh:deactivated ; + sh:maxCount 1 ; # deactivated-maxCount + sh:in ( true false ) ; # deactivated-datatype + ] ; + + sh:property [ + sh:path sh:and ; + sh:node shsh:ListShape ; # and-node + ] ; + sh:property [ + sh:path sh:class ; + sh:nodeKind sh:IRI ; # class-nodeKind + ] ; + sh:property [ + sh:path sh:closed ; + sh:datatype xsd:boolean ; # closed-datatype + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:ignoredProperties ; + sh:node shsh:ListShape ; # ignoredProperties-node + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path ( sh:ignoredProperties [ sh:zeroOrMorePath rdf:rest ] rdf:first ) ; + sh:nodeKind sh:IRI ; # ignoredProperties-members-nodeKind + ] ; + sh:property [ + sh:path sh:datatype ; + sh:nodeKind sh:IRI ; # datatype-nodeKind + sh:maxCount 1 ; # datatype-maxCount + ] ; + sh:property [ + sh:path sh:disjoint ; + sh:nodeKind sh:IRI ; # disjoint-nodeKind + ] ; + sh:property [ + sh:path sh:equals ; + sh:nodeKind sh:IRI ; # equals-nodeKind + ] ; + sh:property [ + sh:path sh:in ; + sh:maxCount 1 ; # in-maxCount + sh:node shsh:ListShape ; # in-node + ] ; + sh:property [ + sh:path sh:languageIn ; + sh:maxCount 1 ; # languageIn-maxCount + sh:node shsh:ListShape ; # languageIn-node + ] ; + sh:property [ + sh:path ( sh:languageIn [ sh:zeroOrMorePath rdf:rest ] rdf:first ) ; + sh:datatype xsd:string ; # languageIn-members-datatype + ] ; + sh:property [ + sh:path sh:lessThan ; + sh:nodeKind sh:IRI ; # lessThan-nodeKind + ] ; + sh:property [ + sh:path sh:lessThanOrEquals ; + sh:nodeKind sh:IRI ; # lessThanOrEquals-nodeKind + ] ; + sh:property [ + sh:path sh:maxCount ; + sh:datatype xsd:integer ; # maxCount-datatype + sh:maxCount 1 ; # maxCount-maxCount + ] ; + sh:property [ + sh:path sh:maxExclusive ; + sh:maxCount 1 ; # maxExclusive-maxCount + sh:nodeKind sh:Literal ; # maxExclusive-nodeKind + ] ; + sh:property [ + sh:path sh:maxInclusive ; + sh:maxCount 1 ; # maxInclusive-maxCount + sh:nodeKind sh:Literal ; # maxInclusive-nodeKind + ] ; + sh:property [ + sh:path sh:maxLength ; + sh:datatype xsd:integer ; # maxLength-datatype + sh:maxCount 1 ; # maxLength-maxCount + ] ; + sh:property [ + sh:path sh:minCount ; + sh:datatype xsd:integer ; # minCount-datatype + sh:maxCount 1 ; # minCount-maxCount + ] ; + sh:property [ + sh:path sh:minExclusive ; + sh:maxCount 1 ; # minExclusive-maxCount + sh:nodeKind sh:Literal ; # minExclusive-nodeKind + ] ; + sh:property [ + sh:path sh:minInclusive ; + sh:maxCount 1 ; # minInclusive-maxCount + sh:nodeKind sh:Literal ; # minInclusive-nodeKind + ] ; + sh:property [ + sh:path sh:minLength ; + sh:datatype xsd:integer ; # minLength-datatype + sh:maxCount 1 ; # minLength-maxCount + ] ; + sh:property [ + sh:path sh:nodeKind ; + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; # nodeKind-in + sh:maxCount 1 ; # nodeKind-maxCount + ] ; + sh:property [ + sh:path sh:or ; + sh:node shsh:ListShape ; # or-node + ] ; + sh:property [ + sh:path sh:pattern ; + sh:datatype xsd:string ; # pattern-datatype + sh:maxCount 1 ; # multiple-parameters + # Not implemented: syntax rule pattern-regex + ] ; + sh:property [ + sh:path sh:flags ; + sh:datatype xsd:string ; # flags-datatype + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:qualifiedMaxCount ; + sh:datatype xsd:integer ; # qualifiedMaxCount-datatype + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:qualifiedMinCount ; + sh:datatype xsd:integer ; # qualifiedMinCount-datatype + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:qualifiedValueShape ; + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:qualifiedValueShapesDisjoint ; + sh:datatype xsd:boolean ; # qualifiedValueShapesDisjoint-datatype + sh:maxCount 1 ; # multiple-parameters + ] ; + sh:property [ + sh:path sh:uniqueLang ; + sh:datatype xsd:boolean ; # uniqueLang-datatype + sh:maxCount 1 ; # uniqueLang-maxCount + ] ; + sh:property [ + sh:path sh:xone ; + sh:node shsh:ListShape ; # xone-node + ] . + +shsh:NodeShapeShape + a sh:NodeShape ; + sh:targetObjectsOf sh:node ; # node-node + sh:property [ + sh:path sh:path ; + sh:maxCount 0 ; # NodeShape-path-maxCount + ] ; + sh:property [ + sh:path sh:lessThan ; + sh:maxCount 0 ; # lessThan-scope + ] ; + sh:property [ + sh:path sh:lessThanOrEquals ; + sh:maxCount 0 ; # lessThanOrEquals-scope + ] ; + sh:property [ + sh:path sh:maxCount ; + sh:maxCount 0 ; # maxCount-scope + ] ; + sh:property [ + sh:path sh:minCount ; + sh:maxCount 0 ; # minCount-scope + ] ; + sh:property [ + sh:path sh:qualifiedValueShape ; + sh:maxCount 0 ; # qualifiedValueShape-scope + ] ; + sh:property [ + sh:path sh:uniqueLang ; + sh:maxCount 0 ; # uniqueLang-scope + ] . + +shsh:PropertyShapeShape + a sh:NodeShape ; + sh:targetObjectsOf sh:property ; # property-node + sh:property [ + sh:path sh:path ; + sh:maxCount 1 ; # path-maxCount + sh:minCount 1 ; # PropertyShape-path-minCount + sh:node shsh:PathShape ; # path-node + ] . + +# Values of sh:and, sh:or and sh:xone must be lists of shapes +shsh:ShapesListShape + a sh:NodeShape ; + sh:targetObjectsOf sh:and ; # and-members-node + sh:targetObjectsOf sh:or ; # or-members-node + sh:targetObjectsOf sh:xone ; # xone-members-node + sh:property [ + sh:path ( [ sh:zeroOrMorePath rdf:rest ] rdf:first ) ; + sh:node shsh:ShapeShape ; + ] . + + +# A path of blank node path syntax, used to simulate recursion +_:PathPath + sh:alternativePath ( + ( [ sh:zeroOrMorePath rdf:rest ] rdf:first ) + ( sh:alternativePath [ sh:zeroOrMorePath rdf:rest ] rdf:first ) + sh:inversePath + sh:zeroOrMorePath + sh:oneOrMorePath + sh:zeroOrOnePath + ) . + +shsh:PathShape + a sh:NodeShape ; + rdfs:label "Path shape"@en ; + rdfs:comment "A shape that can be used to validate the syntax rules of well-formed SHACL paths."@en ; + rdfs:seeAlso ; + sh:property [ + sh:path [ sh:zeroOrMorePath _:PathPath ] ; + sh:node shsh:PathNodeShape ; + ] . + +shsh:PathNodeShape + sh:xone ( # path-metarule + [ sh:nodeKind sh:IRI ] # 2.3.1.1: Predicate path + [ sh:nodeKind sh:BlankNode ; # 2.3.1.2: Sequence path + sh:node shsh:PathListWithAtLeast2Members ; + ] + [ sh:nodeKind sh:BlankNode ; # 2.3.1.3: Alternative path + sh:closed true ; + sh:property [ + sh:path sh:alternativePath ; + sh:node shsh:PathListWithAtLeast2Members ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] + ] + [ sh:nodeKind sh:BlankNode ; # 2.3.1.4: Inverse path + sh:closed true ; + sh:property [ + sh:path sh:inversePath ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] + ] + [ sh:nodeKind sh:BlankNode ; # 2.3.1.5: Zero-or-more path + sh:closed true ; + sh:property [ + sh:path sh:zeroOrMorePath ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] + ] + [ sh:nodeKind sh:BlankNode ; # 2.3.1.6: One-or-more path + sh:closed true ; + sh:property [ + sh:path sh:oneOrMorePath ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] + ] + [ sh:nodeKind sh:BlankNode ; # 2.3.1.7: Zero-or-one path + sh:closed true ; + sh:property [ + sh:path sh:zeroOrOnePath ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] + ] + ) . + +shsh:PathListWithAtLeast2Members + a sh:NodeShape ; + sh:node shsh:ListShape ; + sh:property [ + sh:path [ sh:oneOrMorePath rdf:rest ] ; + sh:minCount 2 ; # 1 other list node plus rdf:nil + ] . + +shsh:ShapesGraphShape + a sh:NodeShape ; + sh:targetObjectsOf sh:shapesGraph ; + sh:nodeKind sh:IRI . # shapesGraph-nodeKind + +shsh:EntailmentShape + a sh:NodeShape ; + sh:targetObjectsOf sh:entailment ; + sh:nodeKind sh:IRI . # entailment-nodeKind diff --git a/testlib/src/main/resources/rdf/ttl/input/shacl.ttl b/testlib/src/main/resources/rdf/ttl/input/shacl.ttl new file mode 100644 index 0000000000..4c0f2220df --- /dev/null +++ b/testlib/src/main/resources/rdf/ttl/input/shacl.ttl @@ -0,0 +1,1665 @@ +# W3C Shapes Constraint Language (SHACL) Vocabulary +# Version from 2017-07-20 + +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . + +@prefix sh: . + +sh: + a owl:Ontology ; + rdfs:label "W3C Shapes Constraint Language (SHACL) Vocabulary"@en ; + rdfs:comment "This vocabulary defines terms used in SHACL, the W3C Shapes Constraint Language."@en ; + sh:declare [ + sh:prefix "sh" ; + sh:namespace "http://www.w3.org/ns/shacl#" ; + ] ; + sh:suggestedShapesGraph . + + +# Shapes vocabulary ----------------------------------------------------------- + +sh:Shape + a rdfs:Class ; + rdfs:label "Shape"@en ; + rdfs:comment "A shape is a collection of constraints that may be targeted for certain nodes."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:NodeShape + a rdfs:Class ; + rdfs:label "Node shape"@en ; + rdfs:comment "A node shape is a shape that specifies constraint that need to be met with respect to focus nodes."@en ; + rdfs:subClassOf sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:PropertyShape + a rdfs:Class ; + rdfs:label "Property shape"@en ; + rdfs:comment "A property shape is a shape that specifies constraints on the values of a focus node for a given property or path."@en ; + rdfs:subClassOf sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:deactivated + a rdf:Property ; + rdfs:label "deactivated"@en ; + rdfs:comment "If set to true then all nodes conform to this."@en ; + # rdfs:domain sh:Shape or sh:SPARQLConstraint + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + +sh:targetClass + a rdf:Property ; + rdfs:label "target class"@en ; + rdfs:comment "Links a shape to a class, indicating that all instances of the class must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range rdfs:Class ; + rdfs:isDefinedBy sh: . + +sh:targetNode + a rdf:Property ; + rdfs:label "target node"@en ; + rdfs:comment "Links a shape to individual nodes, indicating that these nodes must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:targetObjectsOf + a rdf:Property ; + rdfs:label "target objects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all all objects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + +sh:targetSubjectsOf + a rdf:Property ; + rdfs:label "target subjects of"@en ; + rdfs:comment "Links a shape to a property, indicating that all subjects of triples that have the given property as their predicate must conform to the shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + +sh:message + a rdf:Property ; + # domain: sh:Shape or sh:SPARQLConstraint or sh:SPARQLSelectValidator or sh:SPARQLAskValidator + # range: xsd:string or rdf:langString + rdfs:label "message"@en ; + rdfs:comment "A human-readable message (possibly with placeholders for variables) explaining the cause of the result."@en ; + rdfs:isDefinedBy sh: . + +sh:severity + a rdf:Property ; + rdfs:label "severity"@en ; + rdfs:comment "Defines the severity that validation results produced by a shape must have. Defaults to sh:Violation."@en ; + rdfs:domain sh:Shape ; + rdfs:range sh:Severity ; + rdfs:isDefinedBy sh: . + + +# Node kind vocabulary -------------------------------------------------------- + +sh:NodeKind + a rdfs:Class ; + rdfs:label "Node kind"@en ; + rdfs:comment "The class of all node kinds, including sh:BlankNode, sh:IRI, sh:Literal or the combinations of these: sh:BlankNodeOrIRI, sh:BlankNodeOrLiteral, sh:IRIOrLiteral."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:BlankNode + a sh:NodeKind ; + rdfs:label "Blank node"@en ; + rdfs:comment "The node kind of all blank nodes."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrIRI + a sh:NodeKind ; + rdfs:label "Blank node or IRI"@en ; + rdfs:comment "The node kind of all blank nodes or IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:BlankNodeOrLiteral + a sh:NodeKind ; + rdfs:label "Blank node or literal"@en ; + rdfs:comment "The node kind of all blank nodes or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:IRI + a sh:NodeKind ; + rdfs:label "IRI"@en ; + rdfs:comment "The node kind of all IRIs."@en ; + rdfs:isDefinedBy sh: . + +sh:IRIOrLiteral + a sh:NodeKind ; + rdfs:label "IRI or literal"@en ; + rdfs:comment "The node kind of all IRIs or literals."@en ; + rdfs:isDefinedBy sh: . + +sh:Literal + a sh:NodeKind ; + rdfs:label "Literal"@en ; + rdfs:comment "The node kind of all literals."@en ; + rdfs:isDefinedBy sh: . + + +# Results vocabulary ---------------------------------------------------------- + +sh:ValidationReport + a rdfs:Class ; + rdfs:label "Validation report"@en ; + rdfs:comment "The class of SHACL validation reports."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:conforms + a rdf:Property ; + rdfs:label "conforms"@en ; + rdfs:comment "True if the validation did not produce any validation results, and false otherwise."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + +sh:result + a rdf:Property ; + rdfs:label "result"@en ; + rdfs:comment "The validation results contained in a validation report."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:range sh:ValidationResult ; + rdfs:isDefinedBy sh: . + +sh:shapesGraphWellFormed + a rdf:Property ; + rdfs:label "shapes graph well-formed"@en ; + rdfs:comment "If true then the validation engine was certain that the shapes graph has passed all SHACL syntax requirements during the validation process."@en ; + rdfs:domain sh:ValidationReport ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + +sh:AbstractResult + a rdfs:Class ; + rdfs:label "Abstract result"@en ; + rdfs:comment "The base class of validation results, typically not instantiated directly."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:ValidationResult + a rdfs:Class ; + rdfs:label "Validation result"@en ; + rdfs:comment "The class of validation results."@en ; + rdfs:subClassOf sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:Severity + a rdfs:Class ; + rdfs:label "Severity"@en ; + rdfs:comment "The class of validation result severity levels, including violation and warning levels."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:Info + a sh:Severity ; + rdfs:label "Info"@en ; + rdfs:comment "The severity for an informational validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:Violation + a sh:Severity ; + rdfs:label "Violation"@en ; + rdfs:comment "The severity for a violation validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:Warning + a sh:Severity ; + rdfs:label "Warning"@en ; + rdfs:comment "The severity for a warning validation result."@en ; + rdfs:isDefinedBy sh: . + +sh:detail + a rdf:Property ; + rdfs:label "detail"@en ; + rdfs:comment "Links a result with other results that provide more details, for example to describe violations against nested shapes."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:range sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:focusNode + a rdf:Property ; + rdfs:label "focus node"@en ; + rdfs:comment "The focus node that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:resultMessage + a rdf:Property ; + rdfs:label "result message"@en ; + rdfs:comment "Human-readable messages explaining the cause of the result."@en ; + rdfs:domain sh:AbstractResult ; + # range: xsd:string or rdf:langString + rdfs:isDefinedBy sh: . + +sh:resultPath + a rdf:Property ; + rdfs:label "result path"@en ; + rdfs:comment "The path of a validation result, based on the path of the validated property shape."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:resultSeverity + a rdf:Property ; + rdfs:label "result severity"@en ; + rdfs:comment "The severity of the result, e.g. warning."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:range sh:Severity ; + rdfs:isDefinedBy sh: . + +sh:sourceConstraint + a rdf:Property ; + rdfs:label "source constraint"@en ; + rdfs:comment "The constraint that was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + +sh:sourceShape + a rdf:Property ; + rdfs:label "source shape"@en ; + rdfs:comment "The shape that is was validated when the result was produced."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:range sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:sourceConstraintComponent + a rdf:Property ; + rdfs:label "source constraint component"@en ; + rdfs:comment "The constraint component that is the source of the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:range sh:ConstraintComponent ; + rdfs:isDefinedBy sh: . + +sh:value + a rdf:Property ; + rdfs:label "value"@en ; + rdfs:comment "An RDF node that has caused the result."@en ; + rdfs:domain sh:AbstractResult ; + rdfs:isDefinedBy sh: . + + +# Graph properties ------------------------------------------------------------ + +sh:shapesGraph + a rdf:Property ; + rdfs:label "shapes graph"@en ; + rdfs:comment "Shapes graphs that should be used when validating this data graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:range owl:Ontology ; + rdfs:isDefinedBy sh: . + +sh:suggestedShapesGraph + a rdf:Property ; + rdfs:label "suggested shapes graph"@en ; + rdfs:comment "Suggested shapes graphs for this ontology. The values of this property may be used in the absence of specific sh:shapesGraph statements."@en ; + rdfs:domain owl:Ontology ; + rdfs:range owl:Ontology ; + rdfs:isDefinedBy sh: . + +sh:entailment + a rdf:Property ; + rdfs:label "entailment"@en ; + rdfs:comment "An entailment regime that indicates what kind of inferencing is required by a shapes graph."@en ; + rdfs:domain owl:Ontology ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + + +# Path vocabulary ------------------------------------------------------------- + +sh:path + a rdf:Property ; + rdfs:label "path"@en ; + rdfs:comment "Specifies the property path of a property shape."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:inversePath + a rdf:Property ; + rdfs:label "inverse path"@en ; + rdfs:comment "The (single) value of this property represents an inverse path (object to subject)."@en ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:alternativePath + a rdf:Property ; + rdfs:label "alternative path"@en ; + rdfs:comment "The (single) value of this property must be a list of path elements, representing the elements of alternative paths."@en ; + rdfs:range rdf:List ; + rdfs:isDefinedBy sh: . + +sh:zeroOrMorePath + a rdf:Property ; + rdfs:label "zero or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or more times."@en ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:oneOrMorePath + a rdf:Property ; + rdfs:label "one or more path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched one or more times."@en ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:zeroOrOnePath + a rdf:Property ; + rdfs:label "zero or one path"@en ; + rdfs:comment "The (single) value of this property represents a path that is matched zero or one times."@en ; + rdfs:range rdfs:Resource ; + rdfs:isDefinedBy sh: . + + +# Parameters metamodel -------------------------------------------------------- + +sh:Parameterizable + a rdfs:Class ; + rdfs:label "Parameterizable"@en ; + rdfs:comment "Superclass of components that can take parameters, especially functions and constraint components."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:parameter + a rdf:Property ; + rdfs:label "parameter"@en ; + rdfs:comment "The parameters of a function or constraint component."@en ; + rdfs:domain sh:Parameterizable ; + rdfs:range sh:Parameter ; + rdfs:isDefinedBy sh: . + +sh:labelTemplate + a rdf:Property ; + rdfs:label "label template"@en ; + rdfs:comment "Outlines how human-readable labels of instances of the associated Parameterizable shall be produced. The values can contain {?paramName} as placeholders for the actual values of the given parameter."@en ; + rdfs:domain sh:Parameterizable ; + # range: xsd:string or rdf:langString + rdfs:isDefinedBy sh: . + +sh:Parameter + a rdfs:Class ; + rdfs:label "Parameter"@en ; + rdfs:comment "The class of parameter declarations, consisting of a path predicate and (possibly) information about allowed value type, cardinality and other characteristics."@en ; + rdfs:subClassOf sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:optional + a rdf:Property ; + rdfs:label "optional"@en ; + rdfs:comment "Indicates whether a parameter is optional."@en ; + rdfs:domain sh:Parameter ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + + +# Constraint components metamodel --------------------------------------------- + +sh:ConstraintComponent + a rdfs:Class ; + rdfs:label "Constraint component"@en ; + rdfs:comment "The class of constraint components."@en ; + rdfs:subClassOf sh:Parameterizable ; + rdfs:isDefinedBy sh: . + +sh:validator + a rdf:Property ; + rdfs:label "validator"@en ; + rdfs:comment "The validator(s) used to evaluate constraints of either node or property shapes."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:range sh:Validator ; + rdfs:isDefinedBy sh: . + +sh:nodeValidator + a rdf:Property ; + rdfs:label "shape validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a node shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:range sh:Validator ; + rdfs:isDefinedBy sh: . + +sh:propertyValidator + a rdf:Property ; + rdfs:label "property validator"@en ; + rdfs:comment "The validator(s) used to evaluate a constraint in the context of a property shape."@en ; + rdfs:domain sh:ConstraintComponent ; + rdfs:range sh:Validator ; + rdfs:isDefinedBy sh: . + +sh:Validator + a rdfs:Class ; + rdfs:label "Validator"@en ; + rdfs:comment "The class of validators, which provide instructions on how to process a constraint definition. This class serves as base class for the SPARQL-based validators and other possible implementations."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:SPARQLAskValidator + a rdfs:Class ; + rdfs:label "SPARQL ASK validator"@en ; + rdfs:comment "The class of validators based on SPARQL ASK queries. The queries are evaluated for each value node and are supposed to return true if the given node conforms."@en ; + rdfs:subClassOf sh:Validator ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:isDefinedBy sh: . + +sh:SPARQLSelectValidator + a rdfs:Class ; + rdfs:label "SPARQL SELECT validator"@en ; + rdfs:comment "The class of validators based on SPARQL SELECT queries. The queries are evaluated for each focus node and are supposed to produce bindings for all focus nodes that do not conform."@en ; + rdfs:subClassOf sh:Validator ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + + +# Library of Core Constraint Components and their properties ------------------ + +sh:AndConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "And constraint component"@en ; + rdfs:comment "A constraint component that can be used to test whether a value node conforms to all members of a provided list of shapes."@en ; + sh:parameter sh:AndConstraintComponent-and ; + rdfs:isDefinedBy sh: . + +sh:AndConstraintComponent-and + a sh:Parameter ; + sh:path sh:and ; + rdfs:isDefinedBy sh: . + +sh:and + a rdf:Property ; + rdfs:label "and"@en ; + rdfs:comment "RDF list of shapes to validate the value nodes against."@en ; + rdfs:range rdf:List ; + rdfs:isDefinedBy sh: . + + +sh:ClassConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Class constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is an instance of a given type."@en ; + sh:parameter sh:ClassConstraintComponent-class ; + rdfs:isDefinedBy sh: . + +sh:ClassConstraintComponent-class + a sh:Parameter ; + sh:path sh:class ; + sh:nodeKind sh:IRI ; + rdfs:isDefinedBy sh: . + +sh:class + a rdf:Property ; + rdfs:label "class"@en ; + rdfs:comment "The type that all value nodes must have."@en ; + rdfs:range rdfs:Class ; + rdfs:isDefinedBy sh: . + + +sh:ClosedConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Closed constraint component"@en ; + rdfs:comment "A constraint component that can be used to indicate that focus nodes must only have values for those properties that have been explicitly enumerated via sh:property/sh:path."@en ; + sh:parameter sh:ClosedConstraintComponent-closed ; + sh:parameter sh:ClosedConstraintComponent-ignoredProperties ; + rdfs:isDefinedBy sh: . + +sh:ClosedConstraintComponent-closed + a sh:Parameter ; + sh:path sh:closed ; + sh:datatype xsd:boolean ; + rdfs:isDefinedBy sh: . + +sh:ClosedConstraintComponent-ignoredProperties + a sh:Parameter ; + sh:path sh:ignoredProperties ; + sh:optional true ; + rdfs:isDefinedBy sh: . + +sh:closed + a rdf:Property ; + rdfs:label "closed"@en ; + rdfs:comment "If set to true then the shape is closed."@en ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + +sh:ignoredProperties + a rdf:Property ; + rdfs:label "ignored properties"@en ; + rdfs:comment "An optional RDF list of properties that are also permitted in addition to those explicitly enumerated via sh:property/sh:path."@en ; + rdfs:range rdf:List ; # members: rdf:Property + rdfs:isDefinedBy sh: . + + +sh:DatatypeConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Datatype constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the datatype of all value nodes."@en ; + sh:parameter sh:DatatypeConstraintComponent-datatype ; + rdfs:isDefinedBy sh: . + +sh:DatatypeConstraintComponent-datatype + a sh:Parameter ; + sh:path sh:datatype ; + sh:nodeKind sh:IRI ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:datatype + a rdf:Property ; + rdfs:label "datatype"@en ; + rdfs:comment "Specifies an RDF datatype that all value nodes must have."@en ; + rdfs:range rdfs:Datatype ; + rdfs:isDefinedBy sh: . + + +sh:DisjointConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Disjoint constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is disjoint with the the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + sh:parameter sh:DisjointConstraintComponent-disjoint ; + rdfs:isDefinedBy sh: . + +sh:DisjointConstraintComponent-disjoint + a sh:Parameter ; + sh:path sh:disjoint ; + sh:nodeKind sh:IRI ; + rdfs:isDefinedBy sh: . + +sh:disjoint + a rdf:Property ; + rdfs:label "disjoint"@en ; + rdfs:comment "Specifies a property where the set of values must be disjoint with the value nodes."@en ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + + +sh:EqualsConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that the set of value nodes is equal to the set of nodes that have the focus node as subject and the value of a given property as predicate."@en ; + sh:parameter sh:EqualsConstraintComponent-equals ; + rdfs:isDefinedBy sh: . + +sh:EqualsConstraintComponent-equals + a sh:Parameter ; + sh:path sh:equals ; + sh:nodeKind sh:IRI ; + rdfs:isDefinedBy sh: . + +sh:equals + a rdf:Property ; + rdfs:label "equals"@en ; + rdfs:comment "Specifies a property that must have the same values as the value nodes."@en ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + + +sh:HasValueConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Has-value constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that one of the value nodes is a given RDF node."@en ; + sh:parameter sh:HasValueConstraintComponent-hasValue ; + rdfs:isDefinedBy sh: . + +sh:HasValueConstraintComponent-hasValue + a sh:Parameter ; + sh:path sh:hasValue ; + rdfs:isDefinedBy sh: . + +sh:hasValue + a rdf:Property ; + rdfs:label "has value"@en ; + rdfs:comment "Specifies a value that must be among the value nodes."@en ; + rdfs:isDefinedBy sh: . + + +sh:InConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "In constraint component"@en ; + rdfs:comment "A constraint component that can be used to exclusively enumerate the permitted value nodes."@en ; + sh:parameter sh:InConstraintComponent-in ; + rdfs:isDefinedBy sh: . + +sh:InConstraintComponent-in + a sh:Parameter ; + sh:path sh:in ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:in + a rdf:Property ; + rdfs:label "in"@en ; + rdfs:comment "Specifies a list of allowed values so that each value node must be among the members of the given list."@en ; + rdfs:range rdf:List ; + rdfs:isDefinedBy sh: . + + +sh:LanguageInConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Language-in constraint component"@en ; + rdfs:comment "A constraint component that can be used to enumerate language tags that all value nodes must have."@en ; + sh:parameter sh:LanguageInConstraintComponent-languageIn ; + rdfs:isDefinedBy sh: . + +sh:LanguageInConstraintComponent-languageIn + a sh:Parameter ; + sh:path sh:languageIn ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:languageIn + a rdf:Property ; + rdfs:label "language in"@en ; + rdfs:comment "Specifies a list of language tags that all value nodes must have."@en ; + rdfs:range rdf:List ; # members: xsd:string + rdfs:isDefinedBy sh: . + + +sh:LessThanConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Less-than constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that each value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + sh:parameter sh:LessThanConstraintComponent-lessThan ; + rdfs:isDefinedBy sh: . + +sh:LessThanConstraintComponent-lessThan + a sh:Parameter ; + sh:path sh:lessThan ; + sh:nodeKind sh:IRI ; + rdfs:isDefinedBy sh: . + +sh:lessThan + a rdf:Property ; + rdfs:label "less than"@en ; + rdfs:comment "Specifies a property that must have smaller values than the value nodes."@en ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + + +sh:LessThanOrEqualsConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "less-than-or-equals constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node is smaller than all the nodes that have the focus node as subject and the value of a given property as predicate."@en ; + sh:parameter sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals ; + rdfs:isDefinedBy sh: . + +sh:LessThanOrEqualsConstraintComponent-lessThanOrEquals + a sh:Parameter ; + sh:path sh:lessThanOrEquals ; + sh:nodeKind sh:IRI ; + rdfs:isDefinedBy sh: . + +sh:lessThanOrEquals + a rdf:Property ; + rdfs:label "less than or equals"@en ; + rdfs:comment "Specifies a property that must have smaller or equal values than the value nodes."@en ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + + +sh:MaxCountConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum number of value nodes."@en ; + sh:parameter sh:MaxCountConstraintComponent-maxCount ; + rdfs:isDefinedBy sh: . + +sh:MaxCountConstraintComponent-maxCount + a sh:Parameter ; + sh:path sh:maxCount ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:maxCount + a rdf:Property ; + rdfs:label "max count"@en ; + rdfs:comment "Specifies the maximum number of values in the set of value nodes."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + + +sh:MaxExclusiveConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Max-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum exclusive value."@en ; + sh:parameter sh:MaxExclusiveConstraintComponent-maxExclusive ; + rdfs:isDefinedBy sh: . + +sh:MaxExclusiveConstraintComponent-maxExclusive + a sh:Parameter ; + sh:path sh:maxExclusive ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + rdfs:isDefinedBy sh: . + +sh:maxExclusive + a rdf:Property ; + rdfs:label "max exclusive"@en ; + rdfs:comment "Specifies the maximum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + + +sh:MaxInclusiveConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Max-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a maximum inclusive value."@en ; + sh:parameter sh:MaxInclusiveConstraintComponent-maxInclusive ; + rdfs:isDefinedBy sh: . + +sh:MaxInclusiveConstraintComponent-maxInclusive + a sh:Parameter ; + sh:path sh:maxInclusive ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + rdfs:isDefinedBy sh: . + +sh:maxInclusive + a rdf:Property ; + rdfs:label "max inclusive"@en ; + rdfs:comment "Specifies the maximum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + + +sh:MaxLengthConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Max-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the maximum string length of value nodes."@en ; + sh:parameter sh:MaxLengthConstraintComponent-maxLength ; + rdfs:isDefinedBy sh: . + +sh:MaxLengthConstraintComponent-maxLength + a sh:Parameter ; + sh:path sh:maxLength ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:maxLength + a rdf:Property ; + rdfs:label "max length"@en ; + rdfs:comment "Specifies the maximum string length of each value node."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + + +sh:MinCountConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum number of value nodes."@en ; + sh:parameter sh:MinCountConstraintComponent-minCount ; + rdfs:isDefinedBy sh: . + +sh:MinCountConstraintComponent-minCount + a sh:Parameter ; + sh:path sh:minCount ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:minCount + a rdf:Property ; + rdfs:label "min count"@en ; + rdfs:comment "Specifies the minimum number of values in the set of value nodes."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + + +sh:MinExclusiveConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Min-exclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum exclusive value."@en ; + sh:parameter sh:MinExclusiveConstraintComponent-minExclusive ; + rdfs:isDefinedBy sh: . + +sh:MinExclusiveConstraintComponent-minExclusive + a sh:Parameter ; + sh:path sh:minExclusive ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + rdfs:isDefinedBy sh: . + +sh:minExclusive + a rdf:Property ; + rdfs:label "min exclusive"@en ; + rdfs:comment "Specifies the minimum exclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + + +sh:MinInclusiveConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Min-inclusive constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the range of value nodes with a minimum inclusive value."@en ; + sh:parameter sh:MinInclusiveConstraintComponent-minInclusive ; + rdfs:isDefinedBy sh: . + +sh:MinInclusiveConstraintComponent-minInclusive + a sh:Parameter ; + sh:path sh:minInclusive ; + sh:maxCount 1 ; + sh:nodeKind sh:Literal ; + rdfs:isDefinedBy sh: . + +sh:minInclusive + a rdf:Property ; + rdfs:label "min inclusive"@en ; + rdfs:comment "Specifies the minimum inclusive value of each value node."@en ; + rdfs:isDefinedBy sh: . + + +sh:MinLengthConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Min-length constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the minimum string length of value nodes."@en ; + sh:parameter sh:MinLengthConstraintComponent-minLength ; + rdfs:isDefinedBy sh: . + +sh:MinLengthConstraintComponent-minLength + a sh:Parameter ; + sh:path sh:minLength ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:minLength + a rdf:Property ; + rdfs:label "min length"@en ; + rdfs:comment "Specifies the minimum string length of each value node."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + + +sh:NodeConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Node constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given node shape."@en ; + sh:parameter sh:NodeConstraintComponent-node ; + rdfs:isDefinedBy sh: . + +sh:NodeConstraintComponent-node + a sh:Parameter ; + sh:path sh:node ; + rdfs:isDefinedBy sh: . + +sh:node + a rdf:Property ; + rdfs:label "node"@en ; + rdfs:comment "Specifies the node shape that all value nodes must conform to."@en ; + rdfs:range sh:NodeShape ; + rdfs:isDefinedBy sh: . + + +sh:NodeKindConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Node-kind constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the RDF node kind of each value node."@en ; + sh:parameter sh:NodeKindConstraintComponent-nodeKind ; + rdfs:isDefinedBy sh: . + +sh:NodeKindConstraintComponent-nodeKind + a sh:Parameter ; + sh:path sh:nodeKind ; + sh:in ( sh:BlankNode sh:IRI sh:Literal sh:BlankNodeOrIRI sh:BlankNodeOrLiteral sh:IRIOrLiteral ) ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:nodeKind + a rdf:Property ; + rdfs:label "node kind"@en ; + rdfs:comment "Specifies the node kind (e.g. IRI or literal) each value node."@en ; + rdfs:range sh:NodeKind ; + rdfs:isDefinedBy sh: . + + +sh:NotConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Not constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that value nodes do not conform to a given shape."@en ; + sh:parameter sh:NotConstraintComponent-not ; + rdfs:isDefinedBy sh: . + +sh:NotConstraintComponent-not + a sh:Parameter ; + sh:path sh:not ; + rdfs:isDefinedBy sh: . + +sh:not + a rdf:Property ; + rdfs:label "not"@en ; + rdfs:comment "Specifies a shape that the value nodes must not conform to."@en ; + rdfs:range sh:Shape ; + rdfs:isDefinedBy sh: . + + +sh:OrConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Or constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to at least one out of several provided shapes."@en ; + sh:parameter sh:OrConstraintComponent-or ; + rdfs:isDefinedBy sh: . + +sh:OrConstraintComponent-or + a sh:Parameter ; + sh:path sh:or ; + rdfs:isDefinedBy sh: . + +sh:or + a rdf:Property ; + rdfs:label "or"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to at least one of the shapes."@en ; + rdfs:range rdf:List ; # members: sh:Shape ; + rdfs:isDefinedBy sh: . + + +sh:PatternConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Pattern constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that every value node matches a given regular expression."@en ; + sh:parameter sh:PatternConstraintComponent-pattern ; + sh:parameter sh:PatternConstraintComponent-flags ; + rdfs:isDefinedBy sh: . + +sh:PatternConstraintComponent-pattern + a sh:Parameter ; + sh:path sh:pattern ; + sh:datatype xsd:string ; + rdfs:isDefinedBy sh: . + +sh:PatternConstraintComponent-flags + a sh:Parameter ; + sh:path sh:flags ; + sh:datatype xsd:string ; + sh:optional true ; + rdfs:isDefinedBy sh: . + +sh:flags + a rdf:Property ; + rdfs:label "flags"@en ; + rdfs:comment "An optional flag to be used with regular expression pattern matching."@en ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:pattern + a rdf:Property ; + rdfs:label "pattern"@en ; + rdfs:comment "Specifies a regular expression pattern that the string representations of the value nodes must match."@en ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + + +sh:PropertyConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Property constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that all value nodes conform to the given property shape."@en ; + sh:parameter sh:PropertyConstraintComponent-property ; + rdfs:isDefinedBy sh: . + +sh:PropertyConstraintComponent-property + a sh:Parameter ; + sh:path sh:property ; + rdfs:isDefinedBy sh: . + +sh:property + a rdf:Property ; + rdfs:label "property"@en ; + rdfs:comment "Links a shape to its property shapes."@en ; + rdfs:domain sh:Shape ; + rdfs:range sh:PropertyShape ; + rdfs:isDefinedBy sh: . + + +sh:QualifiedMaxCountConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Qualified-max-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified maximum number of value nodes conforms to a given shape."@en ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMaxCountConstraintComponent-qualifiedMaxCount + a sh:Parameter ; + sh:path sh:qualifiedMaxCount ; + sh:datatype xsd:integer ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShape + a sh:Parameter ; + sh:path sh:qualifiedValueShape ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMaxCountConstraintComponent-qualifiedValueShapesDisjoint + a sh:Parameter ; + sh:path sh:qualifiedValueShapesDisjoint ; + sh:datatype xsd:boolean ; + sh:optional true ; + rdfs:isDefinedBy sh: . + + +sh:QualifiedMinCountConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Qualified-min-count constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a specified minimum number of value nodes conforms to a given shape."@en ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedMinCount ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShape ; + sh:parameter sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMinCountConstraintComponent-qualifiedMinCount + a sh:Parameter ; + sh:path sh:qualifiedMinCount ; + sh:datatype xsd:integer ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShape + a sh:Parameter ; + sh:path sh:qualifiedValueShape ; + rdfs:isDefinedBy sh: . + +sh:QualifiedMinCountConstraintComponent-qualifiedValueShapesDisjoint + a sh:Parameter ; + sh:path sh:qualifiedValueShapesDisjoint ; + sh:datatype xsd:boolean ; + sh:optional true ; + rdfs:isDefinedBy sh: . + +sh:qualifiedMaxCount + a rdf:Property ; + rdfs:label "qualified max count"@en ; + rdfs:comment "The maximum number of value nodes that can conform to the shape."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + +sh:qualifiedMinCount + a rdf:Property ; + rdfs:label "qualified min count"@en ; + rdfs:comment "The minimum number of value nodes that must conform to the shape."@en ; + rdfs:range xsd:integer ; + rdfs:isDefinedBy sh: . + +sh:qualifiedValueShape + a rdf:Property ; + rdfs:label "qualified value shape"@en ; + rdfs:comment "The shape that a specified number of values must conform to."@en ; + rdfs:range sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:qualifiedValueShapesDisjoint + a rdf:Property ; + rdfs:label "qualified value shapes disjoint"@en ; + rdfs:comment "Can be used to mark the qualified value shape to be disjoint with its sibling shapes."@en ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + + +sh:UniqueLangConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Unique-languages constraint component"@en ; + rdfs:comment "A constraint component that can be used to specify that no pair of value nodes may use the same language tag."@en ; + sh:parameter sh:UniqueLangConstraintComponent-uniqueLang ; + rdfs:isDefinedBy sh: . + +sh:UniqueLangConstraintComponent-uniqueLang + a sh:Parameter ; + sh:path sh:uniqueLang ; + sh:datatype xsd:boolean ; + sh:maxCount 1 ; + rdfs:isDefinedBy sh: . + +sh:uniqueLang + a rdf:Property ; + rdfs:label "unique languages"@en ; + rdfs:comment "Specifies whether all node values must have a unique (or no) language tag."@en ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy sh: . + + +sh:XoneConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Exactly one constraint component"@en ; + rdfs:comment "A constraint component that can be used to restrict the value nodes so that they conform to exactly one out of several provided shapes."@en ; + sh:parameter sh:XoneConstraintComponent-xone ; + rdfs:isDefinedBy sh: . + +sh:XoneConstraintComponent-xone + a sh:Parameter ; + sh:path sh:xone ; + rdfs:isDefinedBy sh: . + +sh:xone + a rdf:Property ; + rdfs:label "exactly one"@en ; + rdfs:comment "Specifies a list of shapes so that the value nodes must conform to exactly one of the shapes."@en ; + rdfs:range rdf:List ; # members: sh:Shape ; + rdfs:isDefinedBy sh: . + + +# General SPARQL execution support -------------------------------------------- + +sh:SPARQLExecutable + a rdfs:Class ; + rdfs:label "SPARQL executable"@en ; + rdfs:comment "The class of resources that encapsulate a SPARQL query."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:SPARQLAskExecutable + a rdfs:Class ; + rdfs:label "SPARQL ASK executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on an ASK query."@en ; + rdfs:subClassOf sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: . + +sh:ask + a rdf:Property ; + rdfs:label "ask"@en ; + rdfs:comment "The SPARQL ASK query to execute."@en ; + rdfs:domain sh:SPARQLAskExecutable ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:SPARQLConstructExecutable + a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT executable"@en ; + rdfs:comment "The class of SPARQL executables that are based on a CONSTRUCT query."@en ; + rdfs:subClassOf sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: . + +sh:construct + a rdf:Property ; + rdfs:label "construct"@en ; + rdfs:comment "The SPARQL CONSTRUCT query to execute."@en ; + rdfs:domain sh:SPARQLConstructExecutable ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:SPARQLSelectExecutable + a rdfs:Class ; + rdfs:label "SPARQL SELECT executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SELECT query."@en ; + rdfs:subClassOf sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: . + +sh:select + a rdf:Property ; + rdfs:label "select"@en ; + rdfs:comment "The SPARQL SELECT query to execute."@en ; + rdfs:range xsd:string ; + rdfs:domain sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + +sh:SPARQLUpdateExecutable + a rdfs:Class ; + rdfs:label "SPARQL UPDATE executable"@en ; + rdfs:comment "The class of SPARQL executables based on a SPARQL UPDATE."@en ; + rdfs:subClassOf sh:SPARQLExecutable ; + rdfs:isDefinedBy sh: . + +sh:update + a rdf:Property ; + rdfs:label "update"@en ; + rdfs:comment "The SPARQL UPDATE to execute."@en ; + rdfs:domain sh:SPARQLUpdateExecutable ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:prefixes + a rdf:Property ; + rdfs:label "prefixes"@en ; + rdfs:comment "The prefixes that shall be applied before parsing the associated SPARQL query."@en ; + rdfs:domain sh:SPARQLExecutable ; + rdfs:range owl:Ontology ; + rdfs:isDefinedBy sh: . + +sh:PrefixDeclaration + a rdfs:Class ; + rdfs:label "Prefix declaration"@en ; + rdfs:comment "The class of prefix declarations, consisting of pairs of a prefix with a namespace."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:declare + a rdf:Property ; + rdfs:label "declare"@en ; + rdfs:comment "Links a resource with its namespace prefix declarations."@en ; + rdfs:domain owl:Ontology ; + rdfs:range sh:PrefixDeclaration ; + rdfs:isDefinedBy sh: . + +sh:prefix + a rdf:Property ; + rdfs:label "prefix"@en ; + rdfs:comment "The prefix of a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:namespace + a rdf:Property ; + rdfs:label "namespace"@en ; + rdfs:comment "The namespace associated with a prefix in a prefix declaration."@en ; + rdfs:domain sh:PrefixDeclaration ; + rdfs:range xsd:anyURI ; + rdfs:isDefinedBy sh: . + + +# SPARQL-based Constraints support -------------------------------------------- + +sh:SPARQLConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "SPARQL constraint component"@en ; + rdfs:comment "A constraint component that can be used to define constraints based on SPARQL queries."@en ; + sh:parameter sh:SPARQLConstraintComponent-sparql ; + rdfs:isDefinedBy sh: . + +sh:SPARQLConstraintComponent-sparql + a sh:Parameter ; + sh:path sh:sparql ; + rdfs:isDefinedBy sh: . + +sh:sparql + a rdf:Property ; + rdfs:label "constraint (in SPARQL)"@en ; + rdfs:comment "Links a shape with SPARQL constraints."@en ; + rdfs:domain sh:Shape ; + rdfs:range sh:SPARQLConstraint ; + rdfs:isDefinedBy sh: . + +sh:SPARQLConstraint + a rdfs:Class ; + rdfs:label "SPARQL constraint"@en ; + rdfs:comment "The class of constraints based on SPARQL SELECT queries."@en ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + + +# Non-validating constraint properties ---------------------------------------- + +sh:defaultValue + a rdf:Property ; + rdfs:label "default value"@en ; + rdfs:comment "A default value for a property, for example for user interface tools to pre-populate input fields."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:isDefinedBy sh: . + +sh:description + a rdf:Property ; + rdfs:label "description"@en ; + rdfs:comment "Human-readable descriptions for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + # range: xsd:string or rdf:langString + rdfs:isDefinedBy sh: . + +sh:group + a rdf:Property ; + rdfs:label "group"@en ; + rdfs:comment "Can be used to link to a property group to indicate that a property shape belongs to a group of related property shapes."@en ; + rdfs:domain sh:PropertyShape ; + rdfs:range sh:PropertyGroup ; + rdfs:isDefinedBy sh: . + +sh:name + a rdf:Property ; + rdfs:label "name"@en ; + rdfs:comment "Human-readable labels for the property in the context of the surrounding shape."@en ; + rdfs:domain sh:PropertyShape ; + # range: xsd:string or rdf:langString + rdfs:isDefinedBy sh: . + +sh:order + a rdf:Property ; + rdfs:label "order"@en ; + rdfs:comment "Specifies the relative order of this compared to its siblings. For example use 0 for the first, 1 for the second."@en ; + # range: xsd:decimal or xsd:integer ; + rdfs:isDefinedBy sh: . + +sh:PropertyGroup + a rdfs:Class ; + rdfs:label "Property group"@en ; + rdfs:comment "Instances of this class represent groups of property shapes that belong together."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + + +# ----------------------------------------------------------------------------- +# SHACL ADVANCED FEATURES ----------------------------------------------------- +# ----------------------------------------------------------------------------- + + +# Advanced Target vocabulary -------------------------------------------------- + +sh:target + a rdf:Property ; + rdfs:label "target"@en ; + rdfs:comment "Links a shape to a target specified by an extension language, for example instances of sh:SPARQLTarget."@en ; + rdfs:domain sh:Shape ; + rdfs:range sh:Target ; + rdfs:isDefinedBy sh: . + +sh:Target + a rdfs:Class ; + rdfs:label "Target"@en ; + rdfs:comment "The base class of targets such as those based on SPARQL queries."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:TargetType + a rdfs:Class ; + rdfs:label "Target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets. Instances of this are instantiated as values of the sh:target property."@en ; + rdfs:subClassOf rdfs:Class ; + rdfs:subClassOf sh:Parameterizable ; + rdfs:isDefinedBy sh: . + +sh:SPARQLTarget + a rdfs:Class ; + rdfs:label "SPARQL target"@en ; + rdfs:comment "The class of targets that are based on SPARQL queries."@en ; + rdfs:subClassOf sh:Target ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + +sh:SPARQLTargetType + a rdfs:Class ; + rdfs:label "SPARQL target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on SPARQL queries."@en ; + rdfs:subClassOf sh:TargetType ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + + +# Functions Vocabulary -------------------------------------------------------- + +sh:Function + a rdfs:Class ; + rdfs:label "Function"@en ; + rdfs:comment "The class of SHACL functions."@en ; + rdfs:subClassOf sh:Parameterizable ; + rdfs:isDefinedBy sh: . + +sh:returnType + a rdf:Property ; + rdfs:label "return type"@en ; + rdfs:comment "The expected type of values returned by the associated function."@en ; + rdfs:domain sh:Function ; + rdfs:range rdfs:Class ; + rdfs:isDefinedBy sh: . + +sh:SPARQLFunction + a rdfs:Class ; + rdfs:label "SPARQL function"@en ; + rdfs:comment "A function backed by a SPARQL query - either ASK or SELECT."@en ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:SPARQLAskExecutable ; + rdfs:subClassOf sh:SPARQLSelectExecutable ; + rdfs:isDefinedBy sh: . + + +# Result Annotations ---------------------------------------------------------- + +sh:resultAnnotation + a rdf:Property ; + rdfs:label "result annotation"@en ; + rdfs:comment "Links a SPARQL validator with zero or more sh:ResultAnnotation instances, defining how to derive additional result properties based on the variables of the SELECT query."@en ; + rdfs:domain sh:SPARQLSelectValidator ; + rdfs:range sh:ResultAnnotation ; + rdfs:isDefinedBy sh: . + +sh:ResultAnnotation + a rdfs:Class ; + rdfs:label "Result annotation"@en ; + rdfs:comment "A class of result annotations, which define the rules to derive the values of a given annotation property as extra values for a validation result."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:annotationProperty + a rdf:Property ; + rdfs:label "annotation property"@en ; + rdfs:comment "The annotation property that shall be set."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:range rdf:Property ; + rdfs:isDefinedBy sh: . + +sh:annotationValue + a rdf:Property ; + rdfs:label "annotation value"@en ; + rdfs:comment "The (default) values of the annotation property."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:isDefinedBy sh: . + +sh:annotationVarName + a rdf:Property ; + rdfs:label "annotation variable name"@en ; + rdfs:comment "The name of the SPARQL variable from the SELECT clause that shall be used for the values."@en ; + rdfs:domain sh:ResultAnnotation ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + + +# Node Expressions ------------------------------------------------------------ + +sh:this + a rdfs:Resource ; + rdfs:label "this"@en ; + rdfs:comment "A node expression that represents the current focus node."@en ; + rdfs:isDefinedBy sh: . + +sh:filterShape + a rdf:Property ; + rdfs:label "filter shape"@en ; + rdfs:comment "The shape that all input nodes of the expression need to conform to."@en ; + rdfs:range sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:nodes + a rdf:Property ; + rdfs:label "nodes"@en ; + rdfs:comment "The node expression producing the input nodes of a filter shape expression."@en ; + rdfs:isDefinedBy sh: . + +sh:intersection + a rdf:Property ; + rdfs:label "intersection"@en ; + rdfs:comment "A list of node expressions that shall be intersected."@en ; + rdfs:isDefinedBy sh: . + +sh:union + a rdf:Property ; + rdfs:label "union"@en ; + rdfs:comment "A list of node expressions that shall be used together."@en ; + rdfs:isDefinedBy sh: . + + +# Expression Constraints ------------------------------------------------------ + +sh:ExpressionConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "Expression constraint component"@en ; + rdfs:comment "A constraint component that can be used to verify that a given node expression produces true for all value nodes."@en ; + sh:parameter sh:ExpressionConstraintComponent-expression ; + rdfs:isDefinedBy sh: . + +sh:ExpressionConstraintComponent-expression + a sh:Parameter ; + sh:path sh:expression ; + rdfs:isDefinedBy sh: . + +sh:expression + a rdf:Property ; + rdfs:label "expression"@en ; + rdfs:comment "The node expression that must return true for the value nodes."@en ; + rdfs:isDefinedBy sh: . + + +# Rules ----------------------------------------------------------------------- + +sh:Rule + a rdfs:Class ; + rdfs:label "Rule"@en ; + rdfs:comment "The class of SHACL rules. Never instantiated directly."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:rule + a rdf:Property ; + rdfs:label "rule"@en ; + rdfs:comment "The rules linked to a shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range sh:Rule ; + rdfs:isDefinedBy sh: . + +sh:condition + a rdf:Property ; + rdfs:label "condition"@en ; + rdfs:comment "The shapes that the focus nodes need to conform to before a rule is executed on them."@en ; + rdfs:domain sh:Rule ; + rdfs:range sh:Shape ; + rdfs:isDefinedBy sh: . + +sh:TripleRule + a rdfs:Class ; + rdfs:label "A rule based on triple (subject, predicate, object) pattern."@en ; + rdfs:subClassOf sh:Rule ; + rdfs:isDefinedBy sh: . + +sh:subject + a rdf:Property ; + rdfs:label "subject"@en ; + rdfs:comment "An expression producing the resources that shall be inferred as subjects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:predicate + a rdf:Property ; + rdfs:label "predicate"@en ; + rdfs:comment "An expression producing the properties that shall be inferred as predicates."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:object + a rdf:Property ; + rdfs:label "object"@en ; + rdfs:comment "An expression producing the nodes that shall be inferred as objects."@en ; + rdfs:domain sh:TripleRule ; + rdfs:isDefinedBy sh: . + +sh:SPARQLRule + a rdfs:Class ; + rdfs:label "SPARQL CONSTRUCT rule"@en ; + rdfs:comment "The class of SHACL rules based on SPARQL CONSTRUCT queries."@en ; + rdfs:subClassOf sh:Rule ; + rdfs:subClassOf sh:SPARQLConstructExecutable ; + rdfs:isDefinedBy sh: . + + +# SHACL-JS -------------------------------------------------------------------- + +sh:JSExecutable + a rdfs:Class ; + rdfs:label "JavaScript executable"@en ; + rdfs:comment "Abstract base class of resources that declare an executable JavaScript."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:JSTarget + a rdfs:Class ; + rdfs:label "JavaScript target"@en ; + rdfs:comment "The class of targets that are based on JavaScript functions."@en ; + rdfs:subClassOf sh:Target ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:isDefinedBy sh: . + +sh:JSTargetType + a rdfs:Class ; + rdfs:label "JavaScript target type"@en ; + rdfs:comment "The (meta) class for parameterizable targets that are based on JavaScript functions."@en ; + rdfs:subClassOf sh:TargetType ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:isDefinedBy sh: . + +sh:JSConstraint + a rdfs:Class ; + rdfs:label "JavaScript-based constraint"@en ; + rdfs:comment "The class of constraints backed by a JavaScript function."@en ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:isDefinedBy sh: . + +sh:JSConstraintComponent + a sh:ConstraintComponent ; + rdfs:label "JavaScript constraint component"@en ; + rdfs:comment "A constraint component with the parameter sh:js linking to a sh:JSConstraint containing a sh:script."@en ; + sh:parameter sh:JSConstraint-js ; + rdfs:isDefinedBy sh: . + +sh:JSConstraint-js + a sh:Parameter ; + sh:path sh:js ; + rdfs:isDefinedBy sh: . + +sh:js + a rdf:Property ; + rdfs:label "JavaScript constraint"@en ; + rdfs:comment "Constraints expressed in JavaScript." ; + rdfs:range sh:JSConstraint ; + rdfs:isDefinedBy sh: . + +sh:jsFunctionName + a rdf:Property ; + rdfs:label "JavaScript function name"@en ; + rdfs:comment "The name of the JavaScript function to execute."@en ; + rdfs:domain sh:JSExecutable ; + rdfs:range xsd:string ; + rdfs:isDefinedBy sh: . + +sh:jsLibrary + a rdf:Property ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Declares which JavaScript libraries are needed to execute this."@en ; + rdfs:range sh:JSLibrary ; + rdfs:isDefinedBy sh: . + +sh:jsLibraryURL + a rdf:Property ; + rdfs:label "JavaScript library URL"@en ; + rdfs:comment "Declares the URLs of a JavaScript library. This should be the absolute URL of a JavaScript file. Implementations may redirect those to local files."@en ; + rdfs:domain sh:JSLibrary ; + rdfs:range xsd:anyURI ; + rdfs:isDefinedBy sh: . + +sh:JSFunction + a rdfs:Class ; + rdfs:label "JavaScript function"@en ; + rdfs:comment "The class of SHACL functions that execute a JavaScript function when called."@en ; + rdfs:subClassOf sh:Function ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:isDefinedBy sh: . + +sh:JSLibrary + a rdfs:Class ; + rdfs:label "JavaScript library"@en ; + rdfs:comment "Represents a JavaScript library, typically identified by one or more URLs of files to include."@en ; + rdfs:subClassOf rdfs:Resource ; + rdfs:isDefinedBy sh: . + +sh:JSRule + a rdfs:Class ; + rdfs:label "JavaScript rule"@en ; + rdfs:comment "The class of SHACL rules expressed using JavaScript."@en ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Rule ; + rdfs:isDefinedBy sh: . + +sh:JSValidator + a rdfs:Class ; + rdfs:label "JavaScript validator"@en ; + rdfs:comment "A SHACL validator based on JavaScript. This can be used to declare SHACL constraint components that perform JavaScript-based validation when used."@en ; + rdfs:subClassOf sh:JSExecutable ; + rdfs:subClassOf sh:Validator ; + rdfs:isDefinedBy sh: . diff --git a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 b/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 deleted file mode 100644 index 98bf69b7af..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_1.1.0 +++ /dev/null @@ -1,24 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3) - : Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 b/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 deleted file mode 100644 index fc8267fb75..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.cleanWithCustomConf_2.0.1 +++ /dev/null @@ -1,25 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String - ]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 b/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 deleted file mode 100644 index 922a2ccbb9..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.clean_1.1.0 +++ /dev/null @@ -1,16 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String](@annot1 - x: Int @annot2 = 2, - y: Int = 3): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 b/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 deleted file mode 100644 index b838dfea65..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basic.clean_2.0.1 +++ /dev/null @@ -1,18 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String]( - @annot1 - x: Int @annot2 = 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basic.dirty b/testlib/src/main/resources/scala/scalafmt/basic.dirty index 2844c84555..3f106c0126 100644 --- a/testlib/src/main/resources/scala/scalafmt/basic.dirty +++ b/testlib/src/main/resources/scala/scalafmt/basic.dirty @@ -1,4 +1,4 @@ -@ foobar("annot", { +@foobar ("annot", { val x = 2 val y = 2 // y=2 x + y diff --git a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean b/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean deleted file mode 100644 index b838dfea65..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.clean +++ /dev/null @@ -1,18 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a extends b with c { - def foo[T: Int#Double#Triple, R <% String]( - @annot1 - x: Int @annot2 = 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf b/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf deleted file mode 100644 index fc8267fb75..0000000000 --- a/testlib/src/main/resources/scala/scalafmt/basicPost2.0.0.cleanWithCustomConf +++ /dev/null @@ -1,25 +0,0 @@ -@foobar("annot", { - val x = 2 - val y = 2 // y=2 - x + y -}) -object a - extends b - with c { - def foo[ - T: Int#Double#Triple, - R <% String - ]( - @annot1 - x: Int @annot2 = - 2, - y: Int = 3 - ): Int = { - "match" match { - case 1 | 2 => - 3 - case 2 => - 2 - } - } -} diff --git a/testlib/src/main/resources/scala/scalafmt/scalafmt.conf b/testlib/src/main/resources/scala/scalafmt/scalafmt.conf index 5007f5e8ff..a34fa2be05 100644 --- a/testlib/src/main/resources/scala/scalafmt/scalafmt.conf +++ b/testlib/src/main/resources/scala/scalafmt/scalafmt.conf @@ -1,2 +1,4 @@ +version = 3.8.1 +runner.dialect = scala213 style = defaultWithAlign # For pretty alignment. maxColumn = 20 # For my teensy narrow display diff --git a/testlib/src/main/resources/scala/scalafmt/scalafmt.fileoverride.conf b/testlib/src/main/resources/scala/scalafmt/scalafmt.fileoverride.conf new file mode 100644 index 0000000000..ae2f12f789 --- /dev/null +++ b/testlib/src/main/resources/scala/scalafmt/scalafmt.fileoverride.conf @@ -0,0 +1,9 @@ +version = 3.8.1 +runner.dialect = scala213 +style = defaultWithAlign # For pretty alignment. +maxColumn = 200 # For my mega-wide display +fileOverride { + "glob:**/scala/scalafmt/**" { + maxColumn = 20 # For my teensy narrow display + } +} diff --git a/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig b/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig new file mode 100644 index 0000000000..8a7d8d6043 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 + +[*.sh] +indent_style = space +indent_size = 2 +space_redirects = true +switch_case_indent = true diff --git a/testlib/src/main/resources/shell/shfmt/with-config/other.clean b/testlib/src/main/resources/shell/shfmt/with-config/other.clean new file mode 100644 index 0000000000..ba84ece688 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/other.clean @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." + ;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + +echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/with-config/other.sh b/testlib/src/main/resources/shell/shfmt/with-config/other.sh new file mode 100644 index 0000000000..58f97b8276 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/other.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." +;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + + echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/with-config/shfmt.clean b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.clean new file mode 100644 index 0000000000..c1b9b25064 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.clean @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ]; then + unset myArray + else + echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/main/resources/shell/shfmt/with-config/shfmt.sh b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.sh new file mode 100644 index 0000000000..9d15c477d4 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/with-config/shfmt.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ] + then + unset myArray + else +echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/main/resources/shell/shfmt/without-config/other.clean b/testlib/src/main/resources/shell/shfmt/without-config/other.clean new file mode 100644 index 0000000000..cba42806aa --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/other.clean @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in +"apple") + echo "This is a red fruit." + ;; +"banana") + echo "This is a yellow fruit." + ;; +"orange") + echo "This is an orange fruit." + ;; +*) + echo "Unknown fruit." + ;; +esac + +echo "This is some text." >output.txt diff --git a/testlib/src/main/resources/shell/shfmt/without-config/other.sh b/testlib/src/main/resources/shell/shfmt/without-config/other.sh new file mode 100644 index 0000000000..58f97b8276 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/other.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +fruit="apple" + +case $fruit in + "apple") + echo "This is a red fruit." + ;; + "banana") + echo "This is a yellow fruit." +;; + "orange") + echo "This is an orange fruit." + ;; + *) + echo "Unknown fruit." + ;; +esac + + echo "This is some text." > output.txt diff --git a/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean new file mode 100644 index 0000000000..80386d472e --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.clean @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ]; then + unset myArray + else + echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh new file mode 100644 index 0000000000..9d15c477d4 --- /dev/null +++ b/testlib/src/main/resources/shell/shfmt/without-config/shfmt.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +function foo() { + if [ -x $file ]; then + myArray=(item1 item2 item3) + elif [ $file1 -nt $file2 ] + then + unset myArray + else +echo "Usage: $0 file ..." + fi +} + +for ((i = 0; i < 5; i++)); do + read -p r + print -n $r + wait $! +done diff --git a/testlib/src/main/resources/yaml/array_at_root.clean.yaml b/testlib/src/main/resources/yaml/array_at_root.clean.yaml new file mode 100644 index 0000000000..f9bae04114 --- /dev/null +++ b/testlib/src/main/resources/yaml/array_at_root.clean.yaml @@ -0,0 +1,5 @@ +--- +- key1: "value1" + key2: "value2" +- key3: "value3" + key4: "value4" diff --git a/testlib/src/main/resources/yaml/array_at_root.yaml b/testlib/src/main/resources/yaml/array_at_root.yaml new file mode 100644 index 0000000000..b26f6bb246 --- /dev/null +++ b/testlib/src/main/resources/yaml/array_at_root.yaml @@ -0,0 +1,7 @@ +- key1: value1 + key2: value2 + + +- key3: value3 + key4: value4 + diff --git a/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml b/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml new file mode 100644 index 0000000000..0e5571531a --- /dev/null +++ b/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml @@ -0,0 +1,11 @@ +episodes: +- 1 +- 2 +- 3 +- 4 +- 5 +- 6 +- 7 +best-jedi: + name: Obi-Wan + side: light diff --git a/testlib/src/main/resources/yaml/array_with_bracket.clean.yaml b/testlib/src/main/resources/yaml/array_with_bracket.clean.yaml new file mode 100644 index 0000000000..c6f891b9de --- /dev/null +++ b/testlib/src/main/resources/yaml/array_with_bracket.clean.yaml @@ -0,0 +1,12 @@ +--- +episodes: +- 1 +- 2 +- 3 +- 4 +- 5 +- 6 +- 7 +best-jedi: + name: "Obi-Wan" + side: "light" \ No newline at end of file diff --git a/testlib/src/main/resources/yaml/array_with_bracket.yaml b/testlib/src/main/resources/yaml/array_with_bracket.yaml new file mode 100644 index 0000000000..e28373163d --- /dev/null +++ b/testlib/src/main/resources/yaml/array_with_bracket.yaml @@ -0,0 +1,5 @@ +episodes: [1, 2, 3, 4, 5, 6, 7] + + + +best-jedi: {name: Obi-Wan, side: light} \ No newline at end of file diff --git a/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml new file mode 100644 index 0000000000..ff56b4f77a --- /dev/null +++ b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml @@ -0,0 +1,6 @@ +--- +key1: "value1" +key2: "value2" +--- +key3: "value3" +key4: "value4" diff --git a/testlib/src/main/resources/yaml/multiple_documents.clean.yaml b/testlib/src/main/resources/yaml/multiple_documents.clean.yaml new file mode 100644 index 0000000000..3651473955 --- /dev/null +++ b/testlib/src/main/resources/yaml/multiple_documents.clean.yaml @@ -0,0 +1,8 @@ +# Document 1 +key1: value1 +key2: value2 + +--- +# Document 2 +key3: value3 +key4: value4 diff --git a/testlib/src/main/resources/yaml/multiple_documents.yaml b/testlib/src/main/resources/yaml/multiple_documents.yaml new file mode 100644 index 0000000000..3651473955 --- /dev/null +++ b/testlib/src/main/resources/yaml/multiple_documents.yaml @@ -0,0 +1,8 @@ +# Document 1 +key1: value1 +key2: value2 + +--- +# Document 2 +key3: value3 +key4: value4 diff --git a/testlib/src/main/resources/yaml/separator_comments.clean.yaml b/testlib/src/main/resources/yaml/separator_comments.clean.yaml new file mode 100644 index 0000000000..35bb8717dd --- /dev/null +++ b/testlib/src/main/resources/yaml/separator_comments.clean.yaml @@ -0,0 +1,7 @@ +--- +hr: +- "Mark McGwire" +- "Sammy Sosa" +rbi: +- "SS" +- "Ken Griffey" \ No newline at end of file diff --git a/testlib/src/main/resources/yaml/separator_comments.yaml b/testlib/src/main/resources/yaml/separator_comments.yaml new file mode 100644 index 0000000000..0213e66406 --- /dev/null +++ b/testlib/src/main/resources/yaml/separator_comments.yaml @@ -0,0 +1,9 @@ + +--- +hr: + - Mark McGwire + # Following node labeled SS + - &SS Sammy Sosa +rbi: + - *SS # Subsequent occurrence + - Ken Griffey \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/FormatterPropertiesTest.java b/testlib/src/test/java/com/diffplug/spotless/FormatterPropertiesTest.java index 5e47e9ad1a..8333fcc41d 100644 --- a/testlib/src/test/java/com/diffplug/spotless/FormatterPropertiesTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/FormatterPropertiesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,12 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Collection; import java.util.LinkedList; +import java.util.List; import java.util.Properties; +import java.util.stream.Collectors; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; @@ -45,6 +48,14 @@ class FormatterPropertiesTest extends ResourceHarness { RESOURCES_ROOT_DIR + "invalid_xml_properties.xml" }; + private List validPropertiesResources() { + return List.of(VALID_SETTINGS_RESOURCES).stream().filter(it -> !it.endsWith(".xml")).collect(Collectors.toList()); + } + + private List invalidPropertiesResources() { + return List.of(INVALID_SETTINGS_RESOURCES).stream().filter(it -> !it.endsWith(".xml")).collect(Collectors.toList()); + } + private static final String[] VALID_VALUES = { "string", "true", @@ -63,6 +74,18 @@ void differentPropertyFileTypes() throws IOException { } } + @Test + void differentPropertyFileTypes_content_properties() throws IOException { + for (String settingsResource : validPropertiesResources()) { + File settingsFile = createTestFile(settingsResource); + String content = Files.readString(settingsFile.toPath()); + FormatterProperties preferences = FormatterProperties.fromPropertiesContent(List.of(content)); + assertFor(preferences) + .containsSpecificValuesOf(settingsFile) + .containsCommonValueOf(settingsFile); + } + } + @Test void multiplePropertyFiles() throws IOException { LinkedList settingsFiles = new LinkedList<>(); @@ -77,6 +100,22 @@ void multiplePropertyFiles() throws IOException { .containsCommonValueOf(settingsFiles.getLast()); } + @Test + void multiplePropertyFiles_content_properties() throws IOException { + LinkedList settingsFiles = new LinkedList<>(); + LinkedList content = new LinkedList<>(); + for (String settingsResource : validPropertiesResources()) { + File settingsFile = createTestFile(settingsResource); + content.add(Files.readString(settingsFile.toPath())); + settingsFiles.add(settingsFile); + } + FormatterProperties preferences = FormatterProperties.fromPropertiesContent(content); + /* Settings are loaded / overridden in the sequence they are configured. */ + assertFor(preferences) + .containsSpecificValuesOf(settingsFiles) + .containsCommonValueOf(settingsFiles.getLast()); + } + @Test void invalidPropertyFiles() throws IOException { for (String settingsResource : INVALID_SETTINGS_RESOURCES) { @@ -96,6 +135,26 @@ void invalidPropertyFiles() throws IOException { } } + @Test + void invalidPropertyFiles_content_properties() throws IOException { + for (String settingsResource : invalidPropertiesResources()) { + File settingsFile = createTestFile(settingsResource); + String content = Files.readString(settingsFile.toPath()); + boolean exceptionCaught = false; + try { + FormatterProperties.fromPropertiesContent(List.of(content)); + } catch (IllegalArgumentException ex) { + exceptionCaught = true; + assertThat(ex.getMessage()) + .as("IllegalArgumentException does not contain absolute path of file '%s'", settingsFile.getName()) + .contains(settingsFile.getAbsolutePath()); + } + assertThat(exceptionCaught) + .as("No IllegalArgumentException thrown when parsing '%s'", settingsFile.getName()) + .isTrue(); + } + } + @Test void nonExistingFile() throws IOException { String filePath = FileSignature.pathUnixToNative("does/not/exist.properties"); diff --git a/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java b/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java index fc56332b2b..cbd219b33d 100644 --- a/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,26 +17,29 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.diffplug.common.base.StandardSystemProperty; import com.diffplug.spotless.generic.EndWithNewlineStep; class FormatterTest { + @Test + void toUnix() { + Assertions.assertEquals("1\n2\n3", LineEnding.toUnix("1\n2\n3")); + Assertions.assertEquals("1\n2\n3", LineEnding.toUnix("1\r2\r3")); + Assertions.assertEquals("1\n2\n3", LineEnding.toUnix("1\r\n2\r\n3")); + } + // Formatter normally needs to be closed, but no resources will be leaked in this special case @Test void equality() { new SerializableEqualityTester() { private LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy(); private Charset encoding = StandardCharsets.UTF_8; - private Path rootDir = Paths.get(StandardSystemProperty.USER_DIR.value()); private List steps = new ArrayList<>(); - private FormatExceptionPolicy exceptionPolicy = FormatExceptionPolicy.failOnlyOnError(); @Override protected void setupTest(API api) throws Exception { @@ -48,25 +51,8 @@ protected void setupTest(API api) throws Exception { encoding = StandardCharsets.UTF_16; api.areDifferentThan(); - rootDir = rootDir.getParent(); - api.areDifferentThan(); - steps.add(EndWithNewlineStep.create()); api.areDifferentThan(); - - { - FormatExceptionPolicyStrict standard = new FormatExceptionPolicyStrict(); - standard.excludePath("path"); - exceptionPolicy = standard; - api.areDifferentThan(); - } - - { - FormatExceptionPolicyStrict standard = new FormatExceptionPolicyStrict(); - standard.excludeStep("step"); - exceptionPolicy = standard; - api.areDifferentThan(); - } } @Override @@ -74,9 +60,7 @@ protected Formatter create() { return Formatter.builder() .lineEndingsPolicy(lineEndingsPolicy) .encoding(encoding) - .rootDir(rootDir) .steps(steps) - .exceptionPolicy(exceptionPolicy) .build(); } }.testEquals(); diff --git a/testlib/src/test/java/com/diffplug/spotless/JvmTest.java b/testlib/src/test/java/com/diffplug/spotless/JvmTest.java index 2bb374d0ed..801ca2ab24 100644 --- a/testlib/src/test/java/com/diffplug/spotless/JvmTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/JvmTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -93,10 +93,10 @@ void supportListsMinimumJvmIfOnlyHigherJvmSupported() { assertNull(testSupport.getRecommendedFormatterVersion(), "No formatter version is supported"); - for (String fmtVersion : Arrays.asList("1.2", "1.2.3")) { - String proposal = assertThrows(Exception.class, () -> { + for (String fmtVersion : Arrays.asList("1.2", "1.2.3", "1.2-SNAPSHOT", "1.2.3-SNAPSHOT")) { + String proposal = assertThrows(Lint.ShortcutException.class, () -> { testSupport.assertFormatterSupported(fmtVersion); - }).getMessage(); + }).getLints().get(0).getDetail(); assertThat(proposal).contains(String.format("on JVM %d", Jvm.version())); assertThat(proposal).contains(String.format("%s %s requires JVM %d+", TEST_NAME, fmtVersion, higherJvmVersion)); assertThat(proposal).contains(String.format("try %s alternatives", TEST_NAME)); @@ -111,10 +111,10 @@ void supportListsMinimumJvmIfOnlyHigherJvmSupported() { assertThat(proposal).contains(String.format("try %s alternatives", TEST_NAME)); } - for (String fmtVersion : Arrays.asList("1.2.4", "2")) { - String proposal = assertThrows(Exception.class, () -> { + for (String fmtVersion : Arrays.asList("1.2.4", "2", "1.2.5-SNAPSHOT")) { + String proposal = assertThrows(Lint.ShortcutException.class, () -> { testSupport.assertFormatterSupported(fmtVersion); - }).getMessage(); + }).getLints().get(0).getDetail(); assertThat(proposal).contains(String.format("%s %s requires JVM %d+", TEST_NAME, fmtVersion, higherJvmVersion + 1)); proposal = assertThrows(Exception.class, () -> { @@ -132,7 +132,7 @@ void supportProposesFormatterUpgrade() { testSupport.add(Jvm.version() - 2, "1"); testSupport.add(requiredJvm, "2"); testSupport.add(Jvm.version() + 1, "3"); - for (String fmtVersion : Arrays.asList("0", "1", "1.9")) { + for (String fmtVersion : Arrays.asList("0", "1", "1.9", "1.9-SNAPSHOT")) { testSupport.assertFormatterSupported(fmtVersion); String proposal = assertThrows(Exception.class, () -> { @@ -140,9 +140,9 @@ void supportProposesFormatterUpgrade() { throw new Exception("Some test exception"); }).apply(""); }).getMessage(); - assertThat(proposal).contains("not using latest version"); - assertThat(proposal).contains(String.format("on JVM %d+", requiredJvm)); - assertThat(proposal).contains(String.format("upgrade to %s %s", TEST_NAME, "2")); + assertThat(proposal.replace("\r", "")).isEqualTo("My Test Formatter " + fmtVersion + " is currently being used, but outdated.\n" + + "My Test Formatter 2 is the recommended version, which may have fixed this problem.\n" + + "My Test Formatter 2 requires JVM " + (requiredJvm) + "+."); } } @@ -168,7 +168,7 @@ void supportProposesJvmUpgrade() { @Test void supportAllowsExperimentalVersions() { testSupport.add(Jvm.version(), "1.0"); - for (String fmtVersion : Arrays.asList("1", "2.0")) { + for (String fmtVersion : Arrays.asList("1", "2.0", "1.1-SNAPSHOT", "2.0-SNAPSHOT")) { testSupport.assertFormatterSupported(fmtVersion); Exception testException = new Exception("Some test exception"); diff --git a/testlib/src/test/java/com/diffplug/spotless/PaddedCellTest.java b/testlib/src/test/java/com/diffplug/spotless/PaddedCellTest.java index dae41678a4..2820aadeef 100644 --- a/testlib/src/test/java/com/diffplug/spotless/PaddedCellTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/PaddedCellTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,21 +37,20 @@ class PaddedCellTest { @TempDir File rootFolder; - private void misbehaved(FormatterFunc step, String input, PaddedCell.Type expectedOutputType, String steps, String canonical) throws IOException { + private void misbehaved(SerializedFunction step, String input, PaddedCell.Type expectedOutputType, String steps, String canonical) throws IOException { testCase(step, input, expectedOutputType, steps, canonical, true); } - private void wellBehaved(FormatterFunc step, String input, PaddedCell.Type expectedOutputType, String canonical) throws IOException { + private void wellBehaved(SerializedFunction step, String input, PaddedCell.Type expectedOutputType, String canonical) throws IOException { testCase(step, input, expectedOutputType, canonical, canonical, false); } - private void testCase(FormatterFunc step, String input, PaddedCell.Type expectedOutputType, String expectedSteps, String canonical, boolean misbehaved) throws IOException { + private void testCase(SerializedFunction step, String input, PaddedCell.Type expectedOutputType, String expectedSteps, String canonical, boolean misbehaved) throws IOException { List formatterSteps = new ArrayList<>(); - formatterSteps.add(FormatterStep.createNeverUpToDate("step", step)); + formatterSteps.add(NeverUpToDateStep.create("step", step)); try (Formatter formatter = Formatter.builder() .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) .encoding(StandardCharsets.UTF_8) - .rootDir(rootFolder.toPath()) .steps(formatterSteps).build()) { File file = new File(rootFolder, "input"); diff --git a/testlib/src/test/java/com/diffplug/spotless/antlr4/Antlr4FormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/antlr4/Antlr4FormatterStepTest.java index a26cbd4425..0f5b1b0bb7 100644 --- a/testlib/src/test/java/com/diffplug/spotless/antlr4/Antlr4FormatterStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/antlr4/Antlr4FormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,13 @@ import org.junit.jupiter.api.Test; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarness; import com.diffplug.spotless.TestProvisioner; -class Antlr4FormatterStepTest extends ResourceHarness { - +class Antlr4FormatterStepTest { @Test void formatGrammar() throws Throwable { FormatterStep step = Antlr4FormatterStep.create(TestProvisioner.mavenCentral()); - assertOnResources(step, "antlr4/Hello.unformatted.g4", "antlr4/Hello.formatted.g4"); + StepHarness.forStep(step).testResource("antlr4/Hello.unformatted.g4", "antlr4/Hello.formatted.g4"); } - } diff --git a/testlib/src/test/java/com/diffplug/spotless/biome/BiomeStepTest.java b/testlib/src/test/java/com/diffplug/spotless/biome/BiomeStepTest.java new file mode 100644 index 0000000000..87c4d6e91f --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/biome/BiomeStepTest.java @@ -0,0 +1,369 @@ +/* + * Copyright 2023-2024 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.biome; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.StandardSystemProperty; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.ThrowingEx; + +/** + * Tests for formatting files via the {@link BiomeStep}. + */ +class BiomeStepTest extends ResourceHarness { + private static String downloadDir; + + @BeforeAll + static void createDownloadDir() throws IOException { + // We do not want to download Biome each time we execute a test + var userHome = Paths.get(StandardSystemProperty.USER_HOME.value()); + downloadDir = userHome.resolve(".gradle").resolve("rome-dl-test").toAbsolutePath().normalize().toString(); + } + + /** + * Tests that files can be formatted without setting the input language + * explicitly. + */ + @Nested + class AutoDetectLanguage { + /** + * Tests that a *.cjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectCjs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.cjs", "biome/js/fileAfter.cjs"); + } + + /** + * Tests that a *.cts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectCts() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.cts", "biome/ts/fileAfter.cts"); + } + + /** + * Tests that a *.css file can be formatted without setting the input language + * explicitly, using biome 1.8.3 which does require opt-in. + */ + @Test + void testAutoDetectCssExperimental() { + var path = createBiomeConfig("biome/config/css-enabled.json"); + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.8.3", downloadDir.toString()).withConfigPath(path).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/css/fileBefore.css", "biome/css/fileAfter.css"); + } + + /** + * Tests that a *.css file can be formatted without setting the input language + * explicitly, using biome 1.9.0 which does not require opt-in. + */ + @Test + void testAutoDetectCssStable() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.9.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/css/fileBefore.css", "biome/css/fileAfter.css"); + } + + /** + * Tests that a *.js file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.js", "biome/js/fileAfter.js"); + } + + /** + * Tests that a *.js file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJson() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/json/fileBefore.json", "biome/json/fileAfter.json"); + } + + /** + * Tests that a *.jsonc file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJsonc() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/jsonc/fileBefore.jsonc", "biome/jsonc/fileAfter.jsonc"); + } + + /** + * Tests that a *.jsx file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJsx() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.jsx", "biome/js/fileAfter.jsx"); + } + + /** + * Tests that a *.mjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectMjs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.mjs", "biome/js/fileAfter.mjs"); + } + + /** + * Tests that a *.mts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectMts() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.mts", "biome/ts/fileAfter.mts"); + } + + /** + * Tests that a *.ts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectTs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.ts", "biome/ts/fileAfter.ts"); + } + + /** + * Tests that a *.tsx file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectTsx() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.tsx", "biome/ts/fileAfter.tsx"); + } + + /** + * Biome is hard-coded to ignore certain files, such as package.json. Since version 1.5.0, + * the biome CLI does not output any formatted code anymore, whereas previously it printed + * the input as-is. This tests checks that when the biome formatter outputs an empty string, + * the contents of the file to format are used instead. + */ + @Test + void preservesIgnoredFiles() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.5.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/json/package.json", "biome/json/packageAfter.json"); + } + } + + @Nested + class ConfigFile { + /** + * Test formatting with the line width in the config file set to 120. + */ + @Test + void testLineWidth120() { + var path = createBiomeConfig("biome/config/line-width-120.json"); + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withConfigPath(path).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/longLineBefore.js", "biome/js/longLineAfter120.js"); + } + + /** + * Test formatting with the line width in the config file set to 120. + */ + @Test + void testLineWidth80() { + var path = createBiomeConfig("biome/config/line-width-80.json"); + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withConfigPath(path).create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/longLineBefore.js", "biome/js/longLineAfter80.js"); + } + + } + + /** + * Tests that files can be formatted when setting the input language explicitly. + */ + @Nested + class ExplicitLanguage { + /** + * Tests that a *.cjs file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageCjs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.cjs", "biome/js/fileAfter.cjs"); + } + + /** + * Tests that a *.css file can be formatted when setting the input language + * explicitly, using biome 1.8.3 which does require opt-in. + */ + @Test + void testSetLanguageCssExperimental() { + var path = createBiomeConfig("biome/config/css-enabled.json"); + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.8.3", downloadDir.toString()).withConfigPath(path).withLanguage("css").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/css/fileBefore.css", "biome/css/fileAfter.css"); + } + + /** + * Tests that a *.css file can be formatted when setting the input language + * explicitly, using biome 1.9.0 which does not require opt-in. + */ + @Test + void testSetLanguageCssStable() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.9.0", downloadDir.toString()).withLanguage("css").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/css/fileBefore.css", "biome/css/fileAfter.css"); + } + + /** + * Tests that a *.cts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageCts() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.cts", "biome/ts/fileAfter.cts"); + } + + /** + * Tests that a *.js file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageJs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.js", "biome/js/fileAfter.js"); + } + + /** + * Tests that a *.json file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageJson() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("json").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/json/fileBefore.json", "biome/json/fileAfter.json"); + } + + /** + * Tests that a *.jsonc file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageJsonc() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("jsonc").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/jsonc/fileBefore.jsonc", "biome/jsonc/fileAfter.jsonc"); + } + + /** + * Tests that a *.jsx file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageJsx() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("jsx").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.jsx", "biome/js/fileAfter.jsx"); + } + + /** + * Tests that a *.mjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testSetLanguageMjs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/js/fileBefore.mjs", "biome/js/fileAfter.mjs"); + } + + /** + * Tests that a *.mts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageMts() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.mts", "biome/ts/fileAfter.mts"); + } + + /** + * Tests that a *.ts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageTs() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.ts", "biome/ts/fileAfter.ts"); + } + + /** + * Tests that a *.tsx file can be formatted when setting the input language + * explicitly. + */ + @Test + void testSetLanguageTsx() { + var step = BiomeStep.withExeDownload(BiomeFlavor.BIOME, "1.2.0", downloadDir.toString()).withLanguage("tsx").create(); + var stepHarness = StepHarnessWithFile.forStep(BiomeStepTest.this, step); + stepHarness.testResource("biome/ts/fileBefore.tsx", "biome/ts/fileAfter.tsx"); + } + } + + private String createBiomeConfig(String name) { + var config = createTestFile(name).toPath(); + var dir = config.getParent(); + var rome = dir.resolve("biome.json"); + ThrowingEx.run(() -> Files.copy(config, rome)); + return dir.toString(); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/combined/CombinedJavaFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/combined/CombinedJavaFormatStepTest.java new file mode 100644 index 0000000000..086103ce15 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/combined/CombinedJavaFormatStepTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2025 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.combined; + +import static com.diffplug.spotless.TestProvisioner.mavenCentral; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.generic.EndWithNewlineStep; +import com.diffplug.spotless.generic.FenceStep; +import com.diffplug.spotless.generic.IndentStep; +import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; +import com.diffplug.spotless.java.GoogleJavaFormatStep; +import com.diffplug.spotless.java.ImportOrderStep; +import com.diffplug.spotless.java.RemoveUnusedImportsStep; +import com.diffplug.spotless.yaml.SerializeToByteArrayHack; + +public class CombinedJavaFormatStepTest extends ResourceHarness { + + @Test + void checkIssue1679() { + FormatterStep gjf = GoogleJavaFormatStep.create(GoogleJavaFormatStep.defaultVersion(), "AOSP", mavenCentral()); + FormatterStep indentWithSpaces = IndentStep.Type.SPACE.create(); + FormatterStep importOrder = ImportOrderStep.forJava().createFrom(); + FormatterStep removeUnused = RemoveUnusedImportsStep.create(mavenCentral()); + FormatterStep trimTrailing = TrimTrailingWhitespaceStep.create(); + FormatterStep endWithNewLine = EndWithNewlineStep.create(); + FenceStep toggleOffOnPair = FenceStep.named(FenceStep.defaultToggleName()).openClose("formatting:off", "formatting:on"); + try (StepHarness formatter = StepHarness.forSteps( + toggleOffOnPair.preserveWithin(List.of( + new SerializeToByteArrayHack(), + gjf, + indentWithSpaces, + importOrder, + removeUnused, + trimTrailing, + endWithNewLine)))) { + formatter.testResource("combined/issue1679.dirty", "combined/issue1679.clean"); + } + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java index cf0f551485..7e76b4eea1 100644 --- a/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,26 +15,26 @@ */ package com.diffplug.spotless.cpp; -import java.io.File; import java.util.Arrays; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.tag.ClangTest; @ClangTest -class ClangFormatStepTest { +class ClangFormatStepTest extends ResourceHarness { @Test - void test() throws Exception { - try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(ClangFormatStep.withVersion(ClangFormatStep.defaultVersion()).create())) { + void test() { + try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, ClangFormatStep.withVersion(ClangFormatStep.defaultVersion()).create())) { // can't be named java or it gets compiled into .class file - harness.testResource(new File("example.java"), "clang/example.java.dirty", "clang/example.java.clean"); + harness.testResource("example.java", "clang/example.java.dirty", "clang/example.java.clean"); // test every other language clang supports for (String ext : Arrays.asList("c", "cs", "js", "m", "proto")) { String filename = "example." + ext; String root = "clang/" + filename; - harness.testResource(new File(filename), root, root + ".clean"); + harness.testResource(filename, root, root + ".clean"); } } } diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java new file mode 100644 index 0000000000..cf9b9c2ea4 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2020-2025 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.generic; + +import java.io.File; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.StringPrinter; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.StepHarnessWithFile; + +class FenceStepTest extends ResourceHarness { + @Test + void single() { + FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") + .preserveWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness harness = StepHarness.forStep(fence); + harness.test( + StringPrinter.buildStringFromLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I"), + StringPrinter.buildStringFromLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i")); + } + + @Test + void multiple() { + FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") + .preserveWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness harness = StepHarness.forStep(fence); + harness.test( + StringPrinter.buildStringFromLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I", + "spotless:off J K L spotless:on", + "M N O", + "P Q R", + "S T U spotless:off V W", + " X ", + " Y spotless:on Z", + "1 2 3"), + StringPrinter.buildStringFromLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i", + "spotless:off J K L spotless:on", + "m n o", + "p q r", + "s t u spotless:off V W", + " X ", + " Y spotless:on z", + "1 2 3")); + } + + @Test + void broken() { + FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") + .preserveWithin(Arrays.asList(ReplaceStep.create("replace", "spotless:on", "REMOVED"))); + // this fails because uppercase turns spotless:off into SPOTLESS:OFF, etc + StepHarnessWithFile.forStep(this, fence).expectLintsOfFileAndContent("README.md", StringPrinter.buildStringFromLines("A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I")).toBe("L1-6 fence(fenceRemoved) An intermediate step removed a match of spotless:off spotless:on"); + } + + @Test + void andApply() { + FormatterStep fence = FenceStep.named("fence").openClose("", "") + .applyWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness.forStep(fence).test( + StringPrinter.buildStringFromLines( + "A B C", + "", + "D E F", + "", + "G H I"), + StringPrinter.buildStringFromLines( + "A B C", + "", + "d e f", + "", + "G H I")); + } + + static class ToCaseStep implements FormatterStep { + static ToCaseStep upper() { + return new ToCaseStep(true); + } + + static ToCaseStep lower() { + return new ToCaseStep(false); + } + + private final boolean uppercase; + + ToCaseStep(boolean uppercase) { + this.uppercase = uppercase; + } + + @Override + public String getName() { + return uppercase ? "uppercase" : "lowercase"; + } + + @org.jetbrains.annotations.Nullable + @Override + public String format(String rawUnix, File file) throws Exception { + return uppercase ? rawUnix.toUpperCase() : rawUnix.toLowerCase(); + } + + @Override + public void close() throws Exception { + + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ToCaseStep && getName().equals(((ToCaseStep) obj).getName()); + } + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/IndentStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/IndentStepTest.java index c7df4639e3..38503d7182 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/IndentStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/IndentStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,46 +15,42 @@ */ package com.diffplug.spotless.generic; -import java.io.File; - -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.diffplug.spotless.FormatterStep; -import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.SerializableEqualityTester; import com.diffplug.spotless.StepHarness; -class IndentStepTest extends ResourceHarness { +class IndentStepTest { @Test - void tabToTab() throws Throwable { + void tabToTab() { FormatterStep indent = IndentStep.Type.TAB.create(4); - assertOnResources(indent, "indent/IndentedWithTab.test", "indent/IndentedWithTab.test"); + StepHarness.forStep(indent).testResource("indent/IndentedWithTab.test", "indent/IndentedWithTab.test"); } @Test - void spaceToSpace() throws Throwable { + void spaceToSpace() { FormatterStep indent = IndentStep.Type.SPACE.create(4); - assertOnResources(indent, "indent/IndentedWithSpace.test", "indent/IndentedWithSpace.test"); + StepHarness.forStep(indent).testResource("indent/IndentedWithSpace.test", "indent/IndentedWithSpace.test"); } @Test - void spaceToTab() throws Throwable { + void spaceToTab() { FormatterStep indent = IndentStep.Type.TAB.create(4); - assertOnResources(indent, "indent/IndentedWithSpace.test", "indent/IndentedWithTab.test"); + StepHarness.forStep(indent).testResource("indent/IndentedWithSpace.test", "indent/IndentedWithTab.test"); } @Test - void tabToSpace() throws Throwable { + void tabToSpace() { FormatterStep indent = IndentStep.Type.SPACE.create(4); - assertOnResources(indent, "indent/IndentedWithTab.test", "indent/IndentedWithSpace.test"); + StepHarness.forStep(indent).testResource("indent/IndentedWithTab.test", "indent/IndentedWithSpace.test"); } @Test - void doesntClipNewlines() throws Throwable { + void doesntClipNewlines() { FormatterStep indent = IndentStep.Type.SPACE.create(4); String blankNewlines = "\n\n\n\n"; - Assertions.assertEquals(blankNewlines, indent.format(blankNewlines, new File(""))); + StepHarness.forStep(indent).testUnaffected(blankNewlines); } @Test 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 137937beb9..b5e08d2232 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,16 @@ import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.SerializableEqualityTester; import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.StepHarnessWithFile; import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode; class LicenseHeaderStepTest extends ResourceHarness { private static final String FILE_NO_LICENSE = "license/FileWithoutLicenseHeader.test"; - private static final String package_ = "package "; + private static final String package_ = LicenseHeaderStep.DEFAULT_JAVA_HEADER_DELIMITER; private static final String HEADER_WITH_$YEAR = "This is a fake license, $YEAR. ACME corp."; private static final String HEADER_WITH_RANGE_TO_$YEAR = "This is a fake license with range, 2009-$YEAR. ACME corp."; + private static final String HEADER_WITH_$FILE = "This is a fake license, $FILE. ACME corp."; + private static final String HEADER_WITH_$YEAR_$FILE = "This is a fake license, $FILE, $YEAR. ACME corp."; @Test void parseExistingYear() throws Exception { @@ -122,6 +125,13 @@ void should_remove_header_when_empty() throws Throwable { .test(getTestResource("license/HasLicense.test"), getTestResource("license/MissingLicense.test")); } + @Test + void should_skip_lines_matching_predefined_pattern() throws Throwable { + StepHarness.forStep(LicenseHeaderStep.headerDelimiter("", "^(?!