diff --git a/.agents/skills/violation-fixer/SKILL.md b/.agents/skills/violation-fixer/SKILL.md new file mode 100644 index 00000000000..f22596e3b07 --- /dev/null +++ b/.agents/skills/violation-fixer/SKILL.md @@ -0,0 +1,233 @@ +--- +name: violation-fixer +description: Guide for running, interpreting, and fixing code style and analysis violations in grails-core using GrailsCodeStylePlugin, GrailsCodeAnalysisPlugin, and GrailsViolationAggregationPlugin — covering CodeNarc, Checkstyle, PMD, SpotBugs, and JaCoCo +license: Apache-2.0 +--- + + +## What I Do + +- Explain how `GrailsCodeStylePlugin`, `GrailsCodeAnalysisPlugin`, and `GrailsViolationAggregationPlugin` enforce code quality across all 60+ modules. +- Guide you through running style and analysis checks, interpreting the per-tool Markdown violation reports, and fixing each class of violation. +- Describe which tools are always-on vs. opt-in, how to configure them via Gradle properties, and which violations can be auto-fixed. + +## When to Use Me + +Activate this skill when: + +- Running `./gradlew aggregateViolations` and interpreting the resulting `*_VIOLATIONS.md` files. +- Fixing CodeNarc, Checkstyle, PMD, SpotBugs, or Spotless violations reported in those files. +- Configuring code style or analysis tools across the repo (enabling/disabling tools or adjusting rule files). +- Preparing a commit — the plugin output must be clean before merging. + +--- + +## Plugin Overview + +| Plugin | Applied to | Responsibility | +|--------|-----------|----------------| +| `org.apache.grails.gradle.grails-code-style` | Every subproject | Applies Checkstyle and CodeNarc; registers per-project `codeStyle` task; redirects XML reports to root `build/reports/code-style/` | +| `org.apache.grails.gradle.grails-code-analysis` | Every subproject | Applies PMD and SpotBugs (both opt-in); registers per-project `codeAnalysis` task; redirects XML reports to root `build/reports/code-analysis/` | +| `org.apache.grails.gradle.grails-jacoco` | Every subproject | Applies JaCoCo; wires `jacocoTestReport` to run after each `test` task | +| `org.apache.grails.gradle.grails-violation-aggregation` | **Root project only** | Registers `aggregateViolations` and `aggregateJacocoCoverage` tasks; writes Markdown summaries to `build/reports/violations/` | + +--- + +## Key Tasks + +| Task | Scope | Description | +|------|-------|-------------| +| `./gradlew codeStyle` | per-project | Runs Checkstyle and CodeNarc for that project | +| `./gradlew codeAnalysis` | per-project | Runs PMD and/or SpotBugs for that project (when enabled) | +| `./gradlew aggregateViolations` | root | Runs all checks across every module, then writes `*_VIOLATIONS.md` to `build/reports/violations/` | +| `./gradlew aggregateJacocoCoverage` | root | Runs JaCoCo reports across every module, then writes `JACOCO_COVERAGE.md` to `build/reports/violations/` | +| `./gradlew codenarcFix` | per-project | Auto-fixes a subset of CodeNarc violations | + +### Quick commands + +```bash +# Check a single module (style only) +./gradlew :grails-core:codeStyle + +# Check a single module (analysis — must be enabled via properties) +./gradlew :grails-core:codeAnalysis -Pgrails.code-analysis.enabled.pmd=true + +# Full multi-module check + report +./gradlew aggregateViolations + +# Include test sources in style checks +./gradlew aggregateViolations -Pgrails.code-style.enabled.tests=true + +# Include test sources in analysis +./gradlew aggregateViolations -Pgrails.code-analysis.enabled.tests=true + +# Ignore failures (collect reports without failing the build) +./gradlew aggregateViolations -Pgrails.code-style.ignoreFailures=true -Pgrails.code-analysis.ignoreFailures=true + +# Auto-fix some CodeNarc violations before running checks +./gradlew codenarcFix codeStyle + +# JaCoCo coverage report +./gradlew aggregateJacocoCoverage +``` + +--- + +## Output Files + +After running `aggregateViolations`, these files appear under `build/reports/violations/` in the **root project build directory**: + +| File | Tool | Always generated | +|------|------|-----------------| +| `build/reports/violations/CODENARC_VIOLATIONS.md` | CodeNarc | Yes | +| `build/reports/violations/CHECKSTYLE_VIOLATIONS.md` | Checkstyle | Yes | +| `build/reports/violations/PMD_VIOLATIONS.md` | PMD | Yes — contains `No violations found!` when PMD is disabled | +| `build/reports/violations/SPOTBUGS_VIOLATIONS.md` | SpotBugs | Yes — contains `No violations found!` when SpotBugs is disabled | + +After running `aggregateJacocoCoverage`: + +| File | Tool | Generated | +|------|------|-----------| +| `build/reports/violations/JACOCO_COVERAGE.md` | JaCoCo | Only when at least one subproject has a JaCoCo CSV report | + +All reports are inside `build/` and are excluded from version control via `.gitignore`. A clean run produces `No violations found! 🎉` in each style file. **The build must be clean before committing.** + +Each file is a Markdown table grouped by module, with columns: **Class**, **Tool**, **Violation**, **Line**, **Message**. + +--- + +## Tool Details + +### CodeNarc (Groovy — always enabled) + +Rule file: `build/code-style/codenarc/codenarc.groovy` (generated by the plugin during setup; not intended to be edited directly). + +Most common violations and how to fix them: + +| Rule | Fix | +|------|-----| +| `UnnecessaryGString` | Replace `"plain string"` with `'plain string'` | +| `UnnecessarySemicolon` | Remove trailing `;` | +| `SpaceBeforeOpeningBrace` | Add space before `{` → `method() {` | +| `SpaceAroundMapEntryColon` | `[key: value]` not `[key:value]` | +| `ConsecutiveBlankLines` | Collapse 3+ blank lines to 2 | +| `ClassStartsWithBlankLine` | Remove blank line right after `class Foo {` | +| `NoWildcardImports` | Expand `import org.foo.*` to explicit imports | +| `UnusedImport` | Remove imports not referenced in the file | +| `MethodName` | Method names must be camelCase (not `snake_case`) | +| `VariableName` | Variable names must be camelCase | +| `LineLength` | Keep lines ≤ 200 chars (default) | + +Auto-fixable via `codenarcFix`: `ClassStartsWithBlankLine`, `SpaceAroundMapEntryColon`, `UnnecessaryGString`, `UnnecessarySemicolon`, `SpaceBeforeOpeningBrace`, `ConsecutiveBlankLines`. + +### Checkstyle (Java — always enabled) + +Rule file: `build/code-style/checkstyle/checkstyle.xml`. + +Common violations: + +| Rule | Fix | +|------|-----| +| `ImportOrder` | Re-order imports: `java|javax`, then `groovy`, then `jakarta`, then blank, then `io.spring|org.springframework`, then `grails|org.apache.grails|org.grails`, then static imports | +| `AvoidStarImport` | Use explicit class imports | +| `UnusedImports` | Remove unused imports | +| `WhitespaceAround` | Add spaces around operators and keywords | +| `NeedBraces` | Add `{}` to single-statement `if`/`for`/`while` | +| `FileTabCharacter` | Replace tabs with 4 spaces | +| `NewlineAtEndOfFile` | Ensure file ends with `\n` | + +### PMD (Java/Groovy — opt-in) + +Enable: `-Pgrails.code-analysis.enabled.pmd=true` + +Rule file: `build/code-analysis/pmd/pmd.xml`. + +### SpotBugs (Java bytecode — opt-in) + +Enable: `-Pgrails.code-analysis.enabled.spotbugs=true` + +Runs at `Effort.MAX` / `Confidence.HIGH`. Only high-confidence bugs are reported. + +### Spotless (Java auto-formatting — opt-in) + +Enable: `-Pgrails.code-style.enabled.spotless=true` + +Uses Palantir Java Format. Can auto-fix by running: +```bash +./gradlew spotlessApply +``` + +--- + +## Configuration Properties + +All properties can be set in `gradle.properties` or passed as `-P` flags: + +### `grails-code-style` plugin (Checkstyle + CodeNarc) + +| Property | Default | Description | +|----------|---------|-------------| +| `grails.code-style.enabled.checkstyle` | `true` | Enable Checkstyle | +| `grails.code-style.enabled.codenarc` | `true` | Enable CodeNarc | +| `grails.code-style.enabled.spotless` | `false` | Enable Spotless | +| `grails.code-style.enabled.tests` | `false` | Also check test source sets | +| `grails.code-style.ignoreFailures` | `false` | Collect reports without failing build | +| `grails.code-style.codenarc.fix` | `false` | Run `codenarcFix` before CodeNarc tasks | +| `grails.codestyle.dir.checkstyle` | (auto) | Custom path to Checkstyle config dir | +| `grails.codestyle.dir.codenarc` | (auto) | Custom path to CodeNarc config dir | +| `skipCodeStyle` | unset | If present, all style tasks are skipped | + +### `grails-code-analysis` plugin (PMD + SpotBugs) + +| Property | Default | Description | +|----------|---------|-------------| +| `grails.code-analysis.enabled.pmd` | `false` | Enable PMD | +| `grails.code-analysis.enabled.spotbugs` | `false` | Enable SpotBugs | +| `grails.code-analysis.enabled.tests` | `false` | Also analyse test source sets | +| `grails.code-analysis.ignoreFailures` | `false` | Collect reports without failing build | +| `grails.code-analysis.dir.pmd` | (auto) | Custom path to PMD config dir | +| `skipCodeStyle` | unset | If present, all analysis tasks are also skipped | + +--- + +## Fixing Violations Workflow + +1. Run `./gradlew aggregateViolations -Pgrails.code-style.ignoreFailures=true -Pgrails.code-analysis.ignoreFailures=true` +2. Open `build/reports/violations/CODENARC_VIOLATIONS.md` and `build/reports/violations/CHECKSTYLE_VIOLATIONS.md` to see all issues by module +3. For CodeNarc, run `./gradlew codenarcFix` to auto-fix what it can +4. Fix remaining violations manually using the table above +5. Re-run `./gradlew aggregateViolations` and confirm files contain `No violations found! 🎉` +6. The reports are inside `build/` and do not need to be deleted before committing + +--- + +## Reports Directory Structure + +All XML reports are consolidated at: +``` +build/reports/code-style/ ← XML inputs for style aggregation +├── checkstyle/ +│ ├── grails-core-checkstyleMain.xml +│ ├── grails-web-mvc-checkstyleMain.xml +│ └── ... +└── codenarc/ + ├── grails-core-codenarcMain.xml + └── ... + +build/reports/code-analysis/ ← XML inputs for analysis aggregation (if enabled) +├── pmd/ +└── spotbugs/ + +build/reports/violations/ ← Markdown summaries written by aggregateViolations +├── CODENARC_VIOLATIONS.md +├── CHECKSTYLE_VIOLATIONS.md +├── PMD_VIOLATIONS.md +├── SPOTBUGS_VIOLATIONS.md +└── JACOCO_COVERAGE.md ← written by aggregateJacocoCoverage +``` + +The module name is derived from the filename: everything before the last `-` (e.g. `grails-core-checkstyleMain.xml` → module `grails-core`). diff --git a/.github/workflows/codeanalysis.yml b/.github/workflows/codeanalysis.yml new file mode 100644 index 00000000000..cff9c5faf22 --- /dev/null +++ b/.github/workflows/codeanalysis.yml @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "Code Analysis" +on: + push: + branches: + - '[0-9]+.[0-9]+.x' + - '8.0.x-hibernate7.*' + pull_request: + workflow_dispatch: +# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + check_core_projects: + name: "Core Projects" + runs-on: ubuntu-24.04 + steps: + - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica + java-version: 21 + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "🔎 Check Core Projects" + run: ./gradlew aggregateAnalysisViolations --continue -Pgrails.code-analysis.enabled.pmd=true -Pgrails.code-analysis.enabled.spotbugs=true -Pgrails.code-analysis.ignoreFailures=true + - name: "📤 Upload Reports" + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: core-reports + path: build/reports/violations/ + - name: "📋 Publish Code Analysis Report in Job Summary" + if: always() + run: | + echo "## 🔎 Code Analysis Report - Core Projects" >> $GITHUB_STEP_SUMMARY + for report in PMD_VIOLATIONS.md SPOTBUGS_VIOLATIONS.md; do + file="build/reports/violations/$report" + [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true + done + check_gradle_plugin_projects: + name: "Gradle Plugin Projects" + runs-on: ubuntu-24.04 + steps: + - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "☕️ Setup JDK" + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: liberica + java-version: 21 + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + with: + cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + - name: "🔎 Check Gradle Plugin Projects" + working-directory: grails-gradle + run: ./gradlew aggregateAnalysisViolations --continue -Pgrails.code-analysis.enabled.pmd=true -Pgrails.code-analysis.enabled.spotbugs=true -Pgrails.code-analysis.ignoreFailures=true + - name: "📤 Upload Reports" + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: gradle-plugin-reports + path: grails-gradle/build/reports/violations/ + - name: "📋 Publish Code Analysis Report in Job Summary" + if: always() + run: | + echo "## 🔎 Code Analysis Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY + for report in PMD_VIOLATIONS.md SPOTBUGS_VIOLATIONS.md; do + file="grails-gradle/build/reports/violations/$report" + [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true + done diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 31aec3d0e40..28fa6438592 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -18,6 +18,7 @@ on: push: branches: - '[0-9]+.[0-9]+.x' + - '8.0.x-hibernate7.*' pull_request: workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits @@ -44,27 +45,23 @@ jobs: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: "🔎 Check Core Projects" - run: ./gradlew codeStyle - - name: "📤 Upload Failure Reports" + run: ./gradlew aggregateStyleViolations --continue + - name: "📤 Upload Reports" if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: core-reports - path: build/reports/codestyle/ + path: build/reports/violations/ - name: "📋 Publish Code Style Report in Job Summary" if: always() run: | echo "## 🔎 Code Style Report - Core Projects" >> $GITHUB_STEP_SUMMARY - for file in build/reports/codestyle/checkstyle/*.xml build/reports/codestyle/codenarc/*.xml; do - [ -f "$file" ] || continue - if grep -q "> $GITHUB_STEP_SUMMARY - grep "> $GITHUB_STEP_SUMMARY - grep "> $GITHUB_STEP_SUMMARY - fi + for report in CODENARC_VIOLATIONS.md CHECKSTYLE_VIOLATIONS.md; do + file="build/reports/violations/$report" + [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true done - check_gradle_plugin_projects: - name: "Gradle Plugin Projects" + check_forge_projects: + name: "Forge Projects" runs-on: ubuntu-24.04 steps: - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it @@ -81,29 +78,29 @@ jobs: with: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - - name: "🔎 Check Gradle Plugin Projects" - working-directory: grails-gradle + - name: "🔎 Check Forge Projects" + working-directory: grails-forge run: ./gradlew codeStyle - name: "📤 Upload Failure Reports" if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: gradle-plugin-reports - path: grails-gradle/build/reports/codestyle/ + name: forge-reports + path: grails-forge/build/reports/code-style/ - name: "📋 Publish Code Style Report in Job Summary" if: always() run: | - echo "## 🔎 Code Style Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY - for file in grails-gradle/build/reports/codestyle/checkstyle/*.xml grails-gradle/build/reports/codestyle/codenarc/*.xml; do + echo "## 🔎 Code Style Report - Forge Projects" >> $GITHUB_STEP_SUMMARY + for file in grails-forge/build/reports/code-style/checkstyle/*.xml grails-forge/build/reports/code-style/codenarc/*.xml; do [ -f "$file" ] || continue if grep -q "> $GITHUB_STEP_SUMMARY + echo "### ❌ $(basename "$file" .xml)" >> $GITHUB_STEP_SUMMARY grep "> $GITHUB_STEP_SUMMARY grep "> $GITHUB_STEP_SUMMARY fi done - check_grails_forge_projects: - name: "Forge Projects" + check_gradle_plugin_projects: + name: "Gradle Plugin Projects" runs-on: ubuntu-24.04 steps: - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it @@ -120,24 +117,20 @@ jobs: with: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - - name: "🔎 Check Forge Projects" - working-directory: grails-forge - run: ./gradlew codeStyle - - name: "📤 Upload Failure Reports" + - name: "🔎 Check Gradle Plugin Projects" + working-directory: grails-gradle + run: ./gradlew aggregateStyleViolations --continue + - name: "📤 Upload Reports" if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: forge-reports - path: grails-forge/build/reports/codestyle/ + name: gradle-plugin-reports + path: grails-gradle/build/reports/violations/ - name: "📋 Publish Code Style Report in Job Summary" if: always() run: | - echo "## 🔎 Code Style Report - Forge Projects" >> $GITHUB_STEP_SUMMARY - for file in grails-forge/build/reports/codestyle/checkstyle/*.xml grails-forge/build/reports/codestyle/codenarc/*.xml; do - [ -f "$file" ] || continue - if grep -q "> $GITHUB_STEP_SUMMARY - grep "> $GITHUB_STEP_SUMMARY - grep "> $GITHUB_STEP_SUMMARY - fi - done \ No newline at end of file + echo "## 🔎 Code Style Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY + for report in CODENARC_VIOLATIONS.md CHECKSTYLE_VIOLATIONS.md; do + file="grails-gradle/build/reports/violations/$report" + [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true + done diff --git a/build-logic/gradlew.bat b/build-logic/gradlew.bat index aa5f10b069f..24c62d56f2d 100755 --- a/build-logic/gradlew.bat +++ b/build-logic/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle index ef5b010fbd7..b196ee24316 100644 --- a/build-logic/plugins/build.gradle +++ b/build-logic/plugins/build.gradle @@ -38,6 +38,15 @@ dependencies { implementation "${gradleBomDependencies['grails-publish-plugin']}" implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:${gradleProperties.gradleChecksumPluginVersion}" implementation "org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:${gradleProperties.gradleCycloneDxPluginVersion}" + implementation "com.github.spotbugs.snom:spotbugs-gradle-plugin:${gradleProperties.spotbugsPluginVersion}" + + testImplementation "org.spockframework:spock-core:${gradleBomDependencyVersions['gradle-spock.version']}" + testImplementation gradleTestKit() + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() } gradlePlugin { @@ -62,6 +71,14 @@ gradlePlugin { id = 'org.apache.grails.gradle.grails-code-style' implementationClass = 'org.apache.grails.buildsrc.GrailsCodeStylePlugin' } + register('grailsCodeAnalysis') { + id = 'org.apache.grails.gradle.grails-code-analysis' + implementationClass = 'org.apache.grails.buildsrc.GrailsCodeAnalysisPlugin' + } + register('grailsViolationAggregation') { + id = 'org.apache.grails.gradle.grails-violation-aggregation' + implementationClass = 'org.apache.grails.buildsrc.GrailsViolationAggregationPlugin' + } register('groovydocEnhancer') { id = 'org.apache.grails.buildsrc.groovydoc-enhancer' implementationClass = 'org.apache.grails.buildsrc.GroovydocEnhancerPlugin' @@ -79,4 +96,4 @@ gradlePlugin { implementationClass = 'org.apache.grails.buildsrc.GrailsDependencyValidatorPlugin' } } -} \ No newline at end of file +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy index 55ac4e38ab6..906740662a7 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy @@ -22,6 +22,7 @@ package org.apache.grails.buildsrc import groovy.transform.CompileStatic import org.gradle.api.Project import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider @CompileStatic class GradleUtils { @@ -37,6 +38,12 @@ class GradleUtils { asfFile.exists() ? currentDirectory : findAsfRootDir(currentDirectory.dir('../')) } + static Provider booleanProvider(Project project, String name, boolean defaultValue = false) { + project.providers.gradleProperty(name) + .map { it.trim().toBoolean() } + .orElse(defaultValue) + } + static T lookupProperty(Project project, String name, T defaultValue = null) { T v = lookupPropertyByType(project, name, defaultValue?.class) as T return v == null ? defaultValue : v @@ -50,7 +57,7 @@ class GradleUtils { } if (type && (type == Boolean || type == boolean.class)) { def v = findProperty(project, name) - return v == null ? null : Boolean.parseBoolean(v as String) as T + return v == null ? null : (v as String).trim().toBoolean() as T } findProperty(project, name) as T diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy new file mode 100644 index 00000000000..911b85b7815 --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.buildsrc + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory + +@CompileStatic +class GrailsCodeAnalysisExtension { + + /** + * Defaults to rootProject.layout.buildDirectory/code-analysis/pmd. + * Directory for PMD configuration files (e.g. pmd.xml). + */ + final DirectoryProperty pmdDirectory + + /** + * Defaults to rootProject.layout.buildDirectory/reports/code-analysis. + * PMD and SpotBugs XML reports will be written here. + */ + final DirectoryProperty reportsDirectory + + @Inject + GrailsCodeAnalysisExtension(ObjectFactory objects, Project project) { + pmdDirectory = objects.directoryProperty().convention( + project.rootProject.layout.buildDirectory.dir('code-analysis/pmd') + ) + reportsDirectory = objects.directoryProperty().convention( + project.rootProject.layout.buildDirectory.dir('reports/code-analysis') + ) + } +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy new file mode 100644 index 00000000000..6b88166d909 --- /dev/null +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.buildsrc + +import java.nio.file.Files +import java.nio.file.Path + +import groovy.transform.CompileStatic + +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.quality.Pmd +import org.gradle.api.plugins.quality.PmdExtension +import org.gradle.api.plugins.quality.PmdPlugin +import org.gradle.api.provider.Provider + +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort +import com.github.spotbugs.snom.SpotBugsExtension +import com.github.spotbugs.snom.SpotBugsPlugin +import com.github.spotbugs.snom.SpotBugsReport +import com.github.spotbugs.snom.SpotBugsTask + +/** + * Convention plugin for Grails byte code analysis (PMD and SpotBugs). + * Both tools are opt-in; enable via Gradle properties. + */ +@CompileStatic +class GrailsCodeAnalysisPlugin implements Plugin { + + static String PMD_DIR_PROPERTY = 'grails.code-analysis.dir.pmd' + static String PMD_ENABLED_PROPERTY = 'grails.code-analysis.enabled.pmd' + static String PMD_CONFIG_FILE_NAME = 'pmd.xml' + + static String SPOTBUGS_ENABLED_PROPERTY = 'grails.code-analysis.enabled.spotbugs' + + static String IGNORE_FAILURES_PROPERTY = 'grails.code-analysis.ignoreFailures' + static String TEST_ANALYSIS_PROPERTY = 'grails.code-analysis.enabled.tests' + + static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.grails-code-analysis' + + @Override + void apply(Project project) { + initExtension(project) + configurePmd(project) + configureSpotbugs(project) + + // withType returns a live empty collection when the tool is not enabled, + // so these dependsOn calls are safe regardless of whether PMD/SpotBugs are active + project.tasks.register('codeAnalysis') { task -> + task.group = 'verification' + task.description = 'Runs code analysis checks (PMD, SpotBugs)' + task.dependsOn(project.tasks.withType(Pmd)) + task.dependsOn(project.tasks.withType(SpotBugsTask)) + } + } + + private static void initExtension(Project project) { + def gca = project.extensions.create('grailsCodeAnalysis', GrailsCodeAnalysisExtension) + + gca.pmdDirectory.set(project.provider { + def directory = project.hasProperty(PMD_DIR_PROPERTY) ? + project.rootProject.layout.projectDirectory.dir(project.property(PMD_DIR_PROPERTY) as String) : + project.rootProject.layout.buildDirectory.get().dir('code-analysis').dir('pmd') + + def toCreate = directory.asFile.toPath() + Files.createDirectories(toCreate) + + createOrLoad( + toCreate.resolve(PMD_CONFIG_FILE_NAME), + "${BASE_RESOURCE_PATH}/pmd/${PMD_CONFIG_FILE_NAME}", + project + ) + + directory + }) + } + + private static void createOrLoad(Path expectedPath, String defaultResource, Project project) { + def defaultPath = expectedPath.startsWith(project.rootProject.layout.buildDirectory.get().asFile.toPath()) + if (!Files.exists(expectedPath) || expectedPath.size() == 0 || defaultPath) { + def defaultValue = GrailsCodeAnalysisPlugin.getResourceAsStream(defaultResource) + if (!defaultValue) { + throw new IllegalStateException("Could not locate default configuration file: ${defaultResource}") + } + project.logger.info('Replacing code analysis configuration') + expectedPath.text = defaultValue.text + } + } + + static void configurePmd(Project project) { + def pmdEnabled = GradleUtils.booleanProvider(project, PMD_ENABLED_PROPERTY) + if (!pmdEnabled.get()) { + return + } + + project.pluginManager.apply(PmdPlugin) + + def ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY) + def testStylingEnabled = GradleUtils.booleanProvider(project, TEST_ANALYSIS_PROPERTY) + + project.extensions.configure(PmdExtension) { + it.ruleSetFiles = project.files(project.extensions.getByType(GrailsCodeAnalysisExtension).pmdDirectory.file(PMD_CONFIG_FILE_NAME)) + it.ruleSets = [] + it.ignoreFailures = ignoreFailures.get() + it.consoleOutput = true + it.toolVersion = project.findProperty('pmdVersion') + } + + project.tasks.withType(Pmd).configureEach { Pmd task -> + task.group = 'verification' + task.onlyIf { !project.hasProperty('skipCodeStyle') } + task.ignoreFailures = ignoreFailures.get() + + if (task.name.contains('Test') || task.name.contains('test')) { + task.enabled = testStylingEnabled.get() + } + + task.reports.xml.required.set(true) + task.reports.xml.outputLocation.set( + project.extensions.getByType(GrailsCodeAnalysisExtension) + .reportsDirectory.get() + .dir('pmd') + .file("${project.name}-${task.name}.xml") + ) + } + } + + static void configureSpotbugs(Project project) { + def spotbugsEnabled = GradleUtils.booleanProvider(project, SPOTBUGS_ENABLED_PROPERTY) + if (!spotbugsEnabled.get()) { + return + } + + project.pluginManager.apply(SpotBugsPlugin) + + def ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY) + def testStylingEnabled = GradleUtils.booleanProvider(project, TEST_ANALYSIS_PROPERTY) + + project.extensions.configure(SpotBugsExtension) { + it.effort.set(Effort.valueOf('MAX')) + it.reportLevel.set(Confidence.valueOf('HIGH')) + it.ignoreFailures.set(ignoreFailures) + } + + project.tasks.withType(SpotBugsTask).configureEach { SpotBugsTask task -> + task.group = 'verification' + def spotBugsReports = task.reports + def htmlReport = spotBugsReports.maybeCreate('html') + htmlReport.required.set(true) + def xmlReport = spotBugsReports.maybeCreate('xml') + xmlReport.required.set(true) + xmlReport.outputLocation.set( + project.extensions.getByType(GrailsCodeAnalysisExtension) + .reportsDirectory.get() + .dir('spotbugs') + .file("${project.name}-${task.name}.xml") + ) + task.onlyIf { !project.hasProperty('skipCodeStyle') } + + if (task.name.contains('Test') || task.name.contains('test')) { + task.enabled = testStylingEnabled.get() + } + } + } +} diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy index 8d338d34825..b52965ed7f9 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy @@ -30,19 +30,19 @@ import org.gradle.api.model.ObjectFactory class GrailsCodeStyleExtension { /** - * Defaults to project.buildDir/checkstyle. + * Defaults to rootProject.layout.buildDirectory/code-style/checkstyle. * Default checkstyle files will be written here and used from this location. */ final DirectoryProperty checkstyleDirectory /** - * Defaults to project.buildDir/codenarc. + * Defaults to rootProject.layout.buildDirectory/code-style/codenarc. * Default codenarc files will be written here and used from this location. */ final DirectoryProperty codenarcDirectory /** - * Defaults to rootProject.buildDir/reports/codestyle. + * Defaults to rootProject.layout.buildDirectory/reports/code-style. * All Checkstyle and Codenarc reports will be written here. */ final DirectoryProperty reportsDirectory @@ -50,13 +50,13 @@ class GrailsCodeStyleExtension { @Inject GrailsCodeStyleExtension(ObjectFactory objects, Project project) { checkstyleDirectory = objects.directoryProperty().convention( - project.rootProject.layout.buildDirectory.dir('checkstyle') + project.rootProject.layout.buildDirectory.dir('code-style/checkstyle') ) codenarcDirectory = objects.directoryProperty().convention( - project.rootProject.layout.buildDirectory.dir('codenarc') + project.rootProject.layout.buildDirectory.dir('code-style/codenarc') ) reportsDirectory = objects.directoryProperty().convention( - project.rootProject.layout.buildDirectory.dir('reports/codestyle') + project.rootProject.layout.buildDirectory.dir('reports/code-style') ) } } diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy index 67974e3b884..605c8149db3 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy @@ -21,49 +21,59 @@ package org.apache.grails.buildsrc import java.nio.file.Files import java.nio.file.Path +import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.file.Directory import org.gradle.api.plugins.quality.Checkstyle import org.gradle.api.plugins.quality.CheckstyleExtension import org.gradle.api.plugins.quality.CheckstylePlugin import org.gradle.api.plugins.quality.CodeNarc import org.gradle.api.plugins.quality.CodeNarcExtension import org.gradle.api.plugins.quality.CodeNarcPlugin +import org.gradle.api.provider.Provider +/** + * Convention plugin for Grails code style enforcement (Checkstyle and CodeNarc). + */ @CompileStatic class GrailsCodeStylePlugin implements Plugin { + // The directory-override keys keep the legacy 'grails.codestyle.dir.*' names so existing + // project configuration (e.g. grails-forge's custom checkstyle dir) keeps working unchanged. static String CHECKSTYLE_DIR_PROPERTY = 'grails.codestyle.dir.checkstyle' + static String CHECKSTYLE_ENABLED_PROPERTY = 'grails.code-style.enabled.checkstyle' static String CHECKSTYLE_CONFIG_FILE_NAME = 'checkstyle.xml' static String CHECKSTYLE_SUPPRESSION_CONFIG_FILE_NAME = 'checkstyle-suppressions.xml' static String CODENARC_DIR_PROPERTY = 'grails.codestyle.dir.codenarc' + static String CODENARC_ENABLED_PROPERTY = 'grails.code-style.enabled.codenarc' static String CODENARC_CONFIG_FILE_NAME = 'codenarc.groovy' - static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.codestyle' + static String CODENARC_FIX_PROPERTY = 'grails.code-style.codenarc.fix' + + static String IGNORE_FAILURES_PROPERTY = 'grails.code-style.ignoreFailures' + + static String TEST_STYLING_PROPERTY = 'grails.code-style.enabled.tests' + + static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.grails-code-style' @Override void apply(Project project) { initExtension(project) configureCodeStyle(project) - doNotApplyStylingToTests(project) } private static void initExtension(Project project) { def gce = project.extensions.create('grailsCodeStyle', GrailsCodeStyleExtension) - // Unfortunately, the codenarc plugin is still using a non-lazy property. - // Rather than rewrite the plugin to use afterEvaluate, - // this plugin uses properties to override the configuration location by default - + // We are trying to avoid afterEvaluate usage here, so use properties for enabling / disabling gce.checkstyleDirectory.set(project.provider { def directory = project.hasProperty(CHECKSTYLE_DIR_PROPERTY) ? project.rootProject.layout.projectDirectory.dir(project.property(CHECKSTYLE_DIR_PROPERTY) as String) : - project.rootProject.layout.buildDirectory.get().dir('codestyle').dir('checkstyle') + project.rootProject.layout.buildDirectory.get().dir('code-style').dir('checkstyle') def toCreate = directory.asFile.toPath() Files.createDirectories(toCreate) @@ -83,7 +93,7 @@ class GrailsCodeStylePlugin implements Plugin { gce.codenarcDirectory.set(project.provider { def directory = project.hasProperty(CODENARC_DIR_PROPERTY) ? project.rootProject.layout.projectDirectory.dir(project.property(CODENARC_DIR_PROPERTY) as String) : - project.rootProject.layout.buildDirectory.get().dir('codestyle').dir('codenarc') + project.rootProject.layout.buildDirectory.get().dir('code-style').dir('codenarc') def toCreate = directory.asFile.toPath() Files.createDirectories(toCreate) @@ -107,23 +117,6 @@ class GrailsCodeStylePlugin implements Plugin { } } - private static void doNotApplyStylingToTests(Project project) { - project.tasks.named('checkstyleTest') { - it.enabled = false // Do not check test sources at this time - } - - project.afterEvaluate { - // Do not check test sources at this time - ['codenarcIntegrationTest', 'codenarcTest'].each { testTaskName -> - if (project.tasks.names.contains(testTaskName)) { - project.tasks.named(testTaskName) { - it.enabled = false - } - } - } - } - } - private static void configureCodeStyle(Project project) { configureCheckstyle(project) configureCodenarc(project) @@ -131,31 +124,42 @@ class GrailsCodeStylePlugin implements Plugin { project.tasks.register('codeStyle') { it.group = 'verification' it.description = 'Runs code style checks' - it.dependsOn(project.tasks.withType(Checkstyle)) it.dependsOn(project.tasks.withType(CodeNarc)) + it.dependsOn(project.tasks.withType(Checkstyle)) } } static void configureCheckstyle(Project project) { project.pluginManager.apply(CheckstylePlugin) + def ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY) + project.extensions.configure(CheckstyleExtension) { // Explicit `it` is required in extension configuration - it.getConfigDirectory().set(project.extensions.getByType(GrailsCodeStyleExtension).checkstyleDirectory) + it.configDirectory.set(project.extensions.getByType(GrailsCodeStyleExtension).checkstyleDirectory) it.maxWarnings = 0 it.showViolations = true - it.ignoreFailures = false + it.ignoreFailures = ignoreFailures.get() it.toolVersion = project.findProperty('checkstyleVersion') } project.tasks.withType(Checkstyle).configureEach { Checkstyle task -> task.group = 'verification' task.onlyIf { !project.hasProperty('skipCodeStyle') } + task.ignoreFailures = ignoreFailures.get() + + if (task.name.toLowerCase().contains('test')) { + task.enabled = false + } + + // Exclude build directory from Checkstyle task sources to ignore generated sources (e.g. for grails-forge) + // Checked via absolute path to ensure platform-independent separator handling + task.exclude { org.gradle.api.file.FileTreeElement element -> + element.file.absolutePath.contains(File.separator + 'build' + File.separator) + } // Redirect XML report output to a single directory to consolidate - // reports across all subprojects into one known location. - // Include the task name to avoid overlapping outputs when a project has - // multiple source sets (e.g. grails-cache has ast + main). + // reports across all subprojects into one known location task.reports.xml.outputLocation.set( project.extensions.getByType(GrailsCodeStyleExtension) .reportsDirectory.get() @@ -168,20 +172,33 @@ class GrailsCodeStylePlugin implements Plugin { static void configureCodenarc(Project project) { project.pluginManager.apply(CodeNarcPlugin) + registerCodenarcFixTask(project) + + def ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY) + def codenarcFix = GradleUtils.booleanProvider(project, CODENARC_FIX_PROPERTY) + project.extensions.configure(CodeNarcExtension) { it.configFile = project.extensions.getByType(GrailsCodeStyleExtension) - .codenarcDirectory.get().file(CODENARC_CONFIG_FILE_NAME).asFile + .codenarcDirectory.file(CODENARC_CONFIG_FILE_NAME).get().asFile + it.ignoreFailures = ignoreFailures.get() it.toolVersion = project.findProperty('codenarcVersion') } project.tasks.withType(CodeNarc).configureEach { CodeNarc task -> task.group = 'verification' task.onlyIf { !project.hasProperty('skipCodeStyle') } + task.ignoreFailures = ignoreFailures.get() + + if (codenarcFix.get()) { + task.dependsOn('codenarcFix') + } + + if (task.name.toLowerCase().contains('test')) { + task.enabled = false + } // Redirect XML report output to a single directory to consolidate - // reports across all subprojects into one known location. - // Include the task name to avoid overlapping outputs when a project has - // multiple source sets. + // reports across all subprojects into one known location task.reports.xml.required.set(true) task.reports.xml.outputLocation.set( project.extensions.getByType(GrailsCodeStyleExtension) @@ -191,4 +208,60 @@ class GrailsCodeStylePlugin implements Plugin { ) } } + + @CompileDynamic + private static void registerCodenarcFixTask(Project project) { + project.tasks.register('codenarcFix') { + it.group = 'verification' + it.description = 'Automatically fixes some CodeNarc violations' + it.doLast { + project.fileTree(project.projectDir) { + it.include 'src/**/*.groovy' + it.include 'grails-app/**/*.groovy' + it.include 'scripts/**/*.groovy' + it.exclude '**/build/**' + }.each { file -> + String content = file.text + String original = content + + // 1. ClassStartsWithBlankLine + content = content.replaceAll(/(class\s+[^{]+\{\n)([ \t]*[^ \s\n\/])/, '$1\n$2') + + // 2. SpaceAroundMapEntryColon + // (?!:) prevents matching the first : in a :: method reference (e.g. String::trim) + content = content.replaceAll(/([\[,]\s*(?:[\w\-.]+|'[^']+'|"[^"]+")):(?!:)([^\s\/])/, '$1: $2') + content = content.replaceAll(/(\(\s*(?:[\w\-.]+|'[^']+'|"[^"]+")):(?!:)([^\s\/])/, '$1: $2') + + // 3. UnnecessaryGString + // The alternation skips over single-quoted strings so their embedded double-quote + // content is never touched. The (? args -> + if (args[1] == null) { + return args[0] // single-quoted string matched — leave it untouched + } + String inner = args[1] + if (!inner.contains("'")) { + return "'$inner'" + } + return args[0] + } + + // 4. UnnecessarySemicolon + content = content.replaceAll(/(?m);[ \t]*$/, '') + + // 5. SpaceBeforeOpeningBrace + content = content.replaceAll(/(? { + + private static final Logger LOGGER = Logging.getLogger(GrailsViolationAggregationPlugin) + + /** + * Comma-separated list of fully-qualified class-name prefixes to exclude from the aggregated + * JaCoCo coverage report. Configure via {@code -Pgrails.jacoco.aggregation.excludedClassPrefixes=...} + * or in {@code gradle.properties}. + * + *

Defaults to {@link #DEFAULT_JACOCO_EXCLUDED_CLASS_PREFIXES}: the Hibernate 7 support classes + * share fully-qualified names with their Hibernate 5 counterparts, and JaCoCo cannot aggregate + * coverage for two different classes with the same name (it fails with + * "Can't add different class with same name"). Excluding one variant keeps the aggregate valid. + */ + static final String JACOCO_EXCLUDED_CLASS_PREFIXES_PROPERTY = 'grails.jacoco.aggregation.excludedClassPrefixes' + + static final String DEFAULT_JACOCO_EXCLUDED_CLASS_PREFIXES = 'org.grails.orm.hibernate.support.hibernate7.' + + @Override + void apply(Project project) { + if (project != project.rootProject) { + throw new GradleException( + 'GrailsViolationAggregationPlugin must be applied to the root project only. ' + + 'Apply grails-code-style and grails-jacoco to subprojects instead.' + ) + } + + def violationsDir = project.layout.buildDirectory.dir('reports/violations') + def styleXmlDir = project.layout.buildDirectory.dir('reports/code-style') + def analysisXmlDir = project.layout.buildDirectory.dir('reports/code-analysis') + + TaskProvider styleTask = registerStyleAggregation(project, styleXmlDir, violationsDir) + TaskProvider analysisTask = registerAnalysisAggregation(project, analysisXmlDir, violationsDir) + registerJacocoAggregation(project, violationsDir) + + project.tasks.register('aggregateViolations') { Task task -> + task.group = 'verification' + task.description = 'Aggregates all violation reports (style + analysis) into build/reports/violations/' + task.dependsOn(styleTask, analysisTask) + } + } + + private static TaskProvider registerStyleAggregation(Project root, Provider styleXmlDir, Provider violationsDir) { + // Wire property flags as Providers — values are resolved at task execution time, not at apply() time, + // and Providers are configuration-cache safe to capture in task actions + Provider checkStyleTests = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.TEST_STYLING_PROPERTY) + Provider codenarcEnabled = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.CODENARC_ENABLED_PROPERTY, true) + Provider checkstyleEnabled = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.CHECKSTYLE_ENABLED_PROPERTY, true) + + TaskProvider aggregateTask = root.tasks.register('aggregateStyleViolations') { Task task -> + task.group = 'verification' + task.description = 'Aggregates CodeNarc and Checkstyle violation reports into build/reports/violations/' + task.outputs.file(root.file('build/reports/violations/CODENARC_VIOLATIONS.md')) + task.outputs.file(root.file('build/reports/violations/CHECKSTYLE_VIOLATIONS.md')) + task.doLast { + parseStyleViolations(styleXmlDir.get(), violationsDir.get(), + checkStyleTests.get(), codenarcEnabled.get(), checkstyleEnabled.get()) + } + } + root.subprojects { Project sub -> + sub.pluginManager.withPlugin('codenarc') { AppliedPlugin p -> + aggregateTask.configure { Task task -> + task.dependsOn(sub.tasks.withType(CodeNarc)) + } + } + sub.pluginManager.withPlugin('checkstyle') { AppliedPlugin p -> + aggregateTask.configure { Task task -> + task.dependsOn(sub.tasks.withType(Checkstyle)) + } + } + } + aggregateTask + } + + private static TaskProvider registerAnalysisAggregation(Project root, Provider analysisXmlDir, Provider violationsDir) { + Provider checkAnalysisTests = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.TEST_ANALYSIS_PROPERTY) + Provider pmdEnabled = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.PMD_ENABLED_PROPERTY) + Provider spotbugsEnabled = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.SPOTBUGS_ENABLED_PROPERTY) + + TaskProvider aggregateTask = root.tasks.register('aggregateAnalysisViolations') { Task task -> + task.group = 'verification' + task.description = 'Aggregates PMD and SpotBugs violation reports into build/reports/violations/' + task.outputs.file(root.file('build/reports/violations/PMD_VIOLATIONS.md')) + task.outputs.file(root.file('build/reports/violations/SPOTBUGS_VIOLATIONS.md')) + task.doLast { + parseAnalysisViolations(analysisXmlDir.get(), violationsDir.get(), + checkAnalysisTests.get(), pmdEnabled.get(), spotbugsEnabled.get()) + } + } + root.subprojects { Project sub -> + sub.pluginManager.withPlugin('pmd') { AppliedPlugin p -> + aggregateTask.configure { Task task -> + task.dependsOn(sub.tasks.withType(Pmd)) + } + } + sub.pluginManager.withPlugin('com.github.spotbugs') { AppliedPlugin p -> + aggregateTask.configure { Task task -> + task.dependsOn(sub.tasks.withType(SpotBugsTask)) + } + } + } + aggregateTask + } + + private static void registerJacocoAggregation(Project root, Provider violationsDir) { + // Collect all potential CSV paths at configuration time — Project must not be referenced from task actions + FileCollection jacocoCsvFiles = root.files( + root.allprojects.collect { Project p -> p.file('build/reports/jacoco/test/jacocoTestReport.csv') } + ) + + // Resolve the excluded class-name prefixes as a Provider so the value is captured + // configuration-cache-safely and read at task execution time. + Provider> excludedClassPrefixes = root.providers + .gradleProperty(JACOCO_EXCLUDED_CLASS_PREFIXES_PROPERTY) + .orElse(DEFAULT_JACOCO_EXCLUDED_CLASS_PREFIXES) + .map { String value -> + value.split(',').collect { it.trim() }.findAll { !it.isEmpty() } + } + + TaskProvider aggregateTask = root.tasks.register('aggregateJacocoCoverage') { Task task -> + task.group = 'verification' + task.description = 'Aggregates JaCoCo coverage reports from all subprojects into build/reports/violations/' + task.inputs.files(jacocoCsvFiles).optional(true) + task.inputs.property('excludedClassPrefixes', excludedClassPrefixes) + task.outputs.file(root.file('build/reports/violations/JACOCO_COVERAGE.md')) + task.doLast { + parseJacocoCoverage(jacocoCsvFiles, violationsDir.get(), excludedClassPrefixes.get()) + } + } + root.subprojects { Project sub -> + sub.pluginManager.withPlugin('jacoco') { AppliedPlugin p -> + aggregateTask.configure { Task task -> + task.dependsOn(sub.tasks.withType(JacocoReport)) + } + } + } + } + + private static XmlSlurper createSecureSlurper() { + def slurper = new XmlSlurper() + slurper.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true) + slurper.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false) + slurper.setFeature('http://xml.org/sax/features/external-general-entities', false) + slurper.setFeature('http://xml.org/sax/features/external-parameter-entities', false) + slurper.setFeature('http://xml.org/sax/features/namespaces', false) + return slurper + } + + private static String getModule(String fileName) { + def lastDash = fileName.lastIndexOf('-') + lastDash != -1 ? fileName.substring(0, lastDash) : fileName + } + + private static boolean isTestFile(String fileName) { + fileName.toLowerCase().contains('test') || fileName.toLowerCase().contains('integrationtest') + } + + @CompileDynamic + private static void writeReport(Directory violationsDir, String fileName, List violations, String title) { + def outDir = violationsDir.asFile + outDir.mkdirs() + def reportFile = new File(outDir, fileName) + def out = new StringBuilder() + out.append("# ${title}\n") + out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))}\n\n") + + if (violations.isEmpty()) { + out.append('No violations found! 🎉\n') + } else { + def uniqueViolations = violations.unique().sort { v -> "${v.module}:${v.className}:${v.line}" } + def groupedByModule = uniqueViolations.groupBy { it.module }.sort() + groupedByModule.each { module, modViolations -> + out.append("## Module: ${module}\n") + out.append('| Class | Tool | Violation | Line | Message |\n') + out.append('| :--- | :--- | :--- | :--- | :--- |\n') + modViolations.each { v -> + out.append("| ${v.className} | ${v.tool} | ${v.type} | ${v.line} | ${v.message.replaceAll(/\|/, '\\|')} |\n") + } + out.append('\n') + } + } + reportFile.text = out.toString() + LOGGER.lifecycle("Aggregated report generated: ${reportFile.absolutePath}") + } + + @CompileDynamic + private static void parseStyleViolations(Directory styleXmlDir, Directory violationsDir, + boolean checkStyleTests, boolean codenarcEnabled, boolean checkstyleEnabled) { + def slurper = createSecureSlurper() + + def shouldSkipClass = { boolean includeTests, String className, String filePath = null -> + if (includeTests) { + return false + } + if (filePath && (filePath.contains('src/test/') || filePath.contains('src/integrationTest/'))) { + return true + } + !filePath && (className.contains('Spec') || className.contains('Test') || className.contains('Tests')) + } + + // CodeNarc + def codenarcViolations = [] + def codenarcDir = styleXmlDir.dir('codenarc').asFile + if (codenarcDir.exists() && codenarcEnabled) { + codenarcDir.eachFileMatch(~/.*\.xml/) { file -> + if (file.size() == 0 || (!checkStyleTests && isTestFile(file.name))) { + return + } + def module = getModule(file.name) + def xml = slurper.parse(file) + xml.Package.each { pkg -> + pkg.File.each { f -> + def pkgName = pkg.@name.text() + def fileName = f.@name.text() + def className = pkgName ? "${pkgName}.${fileName}" : fileName + className = className.replace('.groovy', '').replace('.java', '') + if (shouldSkipClass(checkStyleTests, className, f.@name.text())) { + return + } + f.Violation.each { v -> + codenarcViolations << [ + module : module, + className: className, + tool : 'CodeNarc', + type : v.@ruleName.text(), + line : v.@lineNumber.text(), + message : v.Message.text().trim() + ] + } + } + } + } + } + writeReport(violationsDir, 'CODENARC_VIOLATIONS.md', codenarcViolations, 'CodeNarc Violations Summary') + + // Checkstyle + def checkstyleViolations = [] + def checkstyleDir = styleXmlDir.dir('checkstyle').asFile + if (checkstyleDir.exists() && checkstyleEnabled) { + checkstyleDir.eachFileMatch(~/.*\.xml/) { file -> + if (file.size() == 0 || (!checkStyleTests && isTestFile(file.name))) { + return + } + def module = getModule(file.name) + def xml = slurper.parse(file) + xml.file.each { f -> + def filePath = f.@name.text() + def className = filePath.contains('src/main/groovy/') ? filePath.split('src/main/groovy/')[1] : + filePath.contains('src/main/java/') ? filePath.split('src/main/java/')[1] : + filePath.contains('src/test/groovy/') ? filePath.split('src/test/groovy/')[1] : + filePath.contains('src/test/java/') ? filePath.split('src/test/java/')[1] : + filePath.split('/').last() + className = className.replace('.groovy', '').replace('.java', '').replace('/', '.') + if (shouldSkipClass(checkStyleTests, className)) { + return + } + f.error.each { e -> + checkstyleViolations << [ + module : module, + className: className, + tool : 'Checkstyle', + type : e.@source.text().split(/\./).last(), + line : e.@line.text(), + message : e.@message.text().trim() + ] + } + } + } + } + writeReport(violationsDir, 'CHECKSTYLE_VIOLATIONS.md', checkstyleViolations, 'Checkstyle Violations Summary') + } + + @CompileDynamic + private static void parseAnalysisViolations(Directory analysisXmlDir, Directory violationsDir, + boolean checkAnalysisTests, boolean pmdEnabled, boolean spotbugsEnabled) { + def slurper = createSecureSlurper() + + def shouldSkipClass = { boolean includeTests, String className -> + if (includeTests) { + return false + } + className.contains('Spec') || className.contains('Test') || className.contains('Tests') + } + + // PMD + def pmdViolations = [] + def pmdDir = analysisXmlDir.dir('pmd').asFile + if (pmdDir.exists() && pmdEnabled) { + pmdDir.eachFileMatch(~/.*\.xml/) { file -> + if (file.size() == 0 || (!checkAnalysisTests && isTestFile(file.name))) { + return + } + def module = getModule(file.name) + def xml = slurper.parse(file) + xml.file.each { f -> + f.violation.each { v -> + def className = "${v.@package}.${v.@class}" + if (shouldSkipClass(checkAnalysisTests, className)) { + return + } + pmdViolations << [ + module : module, + className: className, + tool : 'PMD', + type : v.@rule.text(), + line : v.@beginline.text(), + message : v.text().trim() + ] + } + } + } + } + writeReport(violationsDir, 'PMD_VIOLATIONS.md', pmdViolations, 'PMD Violations Summary') + + // SpotBugs + def spotbugsViolations = [] + def spotbugsDir = analysisXmlDir.dir('spotbugs').asFile + if (spotbugsDir.exists() && spotbugsEnabled) { + spotbugsDir.eachFileMatch(~/.*\.xml/) { file -> + if (file.size() == 0 || (!checkAnalysisTests && isTestFile(file.name))) { + return + } + def module = getModule(file.name) + def xml = slurper.parse(file) + xml.BugInstance.each { b -> + def className = b.Class.@classname.text() + if (shouldSkipClass(checkAnalysisTests, className)) { + return + } + spotbugsViolations << [ + module : module, + className: className, + tool : 'SpotBugs', + type : b.@type.text(), + line : b.SourceLine.@start.text(), + message : b.LongMessage.text().trim() + ] + } + } + } + writeReport(violationsDir, 'SPOTBUGS_VIOLATIONS.md', spotbugsViolations, 'SpotBugs Violations Summary') + } + + @CompileDynamic + private static void parseJacocoCoverage(FileCollection csvFiles, Directory violationsDir, List excludedClassPrefixes) { + def jacocoCoverage = [] + csvFiles.each { File csvReport -> + if (csvReport.exists()) { + LOGGER.debug("Processing JaCoCo report: ${csvReport.absolutePath}") + csvReport.splitEachLine(',') { fields -> + if (fields.size() < 5 || fields[0] == 'GROUP') { + return + } + def module = fields[0] + def pkg = fields[1] + def clazz = fields[2] + def missedStr = fields[3] + def coveredStr = fields[4] + + if (missedStr.isNumber() && coveredStr.isNumber()) { + def m = missedStr.toInteger() + def c = coveredStr.toInteger() + def total = m + c + def percent = total > 0 ? (c * 100 / total).round(2) : 100.0 + + jacocoCoverage << [ + module : module, + className: "${pkg}.${clazz}", + percent : percent + ] + } + } + } + } + + if (jacocoCoverage.isEmpty()) { + LOGGER.info('No JaCoCo coverage reports found to aggregate') + return + } + + // Drop classes whose fully-qualified names collide across Hibernate variants (see + // JACOCO_EXCLUDED_CLASS_PREFIXES_PROPERTY) so the aggregate report stays valid. + if (excludedClassPrefixes) { + jacocoCoverage.removeIf { entry -> excludedClassPrefixes.any { prefix -> entry.className.startsWith(prefix) } } + } + + def outDir = violationsDir.asFile + outDir.mkdirs() + def reportFile = new File(outDir, 'JACOCO_COVERAGE.md') + def out = new StringBuilder() + out.append('# JaCoCo Coverage Report\n') + out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))}\n\n") + + def groupedByModule = jacocoCoverage.groupBy { it.module }.sort() + groupedByModule.each { module, coverageList -> + out.append("## Module: ${module}\n") + out.append('| Class | % Instructions Covered |\n') + out.append('| :--- | :--- |\n') + coverageList.sort { it.percent }.each { c -> + out.append("| ${c.className} | ${c.percent}% |\n") + } + out.append('\n') + } + reportFile.text = out.toString() + LOGGER.lifecycle("Aggregated JaCoCo report generated: ${reportFile.absolutePath}") + } +} diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml new file mode 100644 index 00000000000..cf494a71258 --- /dev/null +++ b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml @@ -0,0 +1,31 @@ + + + + + PMD ruleset for the Grails codebase + + + + + + diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle-suppressions.xml similarity index 100% rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle-suppressions.xml diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle.xml similarity index 98% rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle.xml rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle.xml index e6034dc6c77..d88270afa10 100644 --- a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle.xml +++ b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle.xml @@ -20,6 +20,8 @@ + + diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/codenarc/codenarc.groovy b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/codenarc/codenarc.groovy similarity index 100% rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/codenarc/codenarc.groovy rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/codenarc/codenarc.groovy diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy new file mode 100644 index 00000000000..46c9246992a --- /dev/null +++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.buildsrc + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Specification +import spock.lang.TempDir +import java.nio.file.Path + +class GrailsCodeStylePluginSpec extends Specification { + @TempDir + Path testProjectDir + + File buildFile + File groovyFile + + def setup() { + buildFile = testProjectDir.resolve('build.gradle').toFile() + buildFile << """ + plugins { + id 'groovy' + id 'org.apache.grails.gradle.grails-code-style' + } + + // Minimal configuration for the plugin + repositories { + mavenCentral() + } + """ + + testProjectDir.resolve('src/main/groovy').toFile().mkdirs() + groovyFile = testProjectDir.resolve('src/main/groovy/Test.groovy').toFile() + } + + def "test codenarcFix task fixes violations"() { + given: "a file with violations" + groovyFile.text = """package org.test + +class Test{ + def map = [key:"value"] + def str = "unnecessary gstring" + def semi = "semicolon"; + def lines = 1 + + + def other = 2 +} +""" + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "violations are fixed" + def fixedContent = groovyFile.text + fixedContent.contains('class Test {') // SpaceBeforeOpeningBrace + fixedContent.contains('class Test {\n\n def map') + fixedContent.contains("[key: 'value']") // SpaceAroundMapEntryColon and UnnecessaryGString + fixedContent.contains("'unnecessary gstring'") // UnnecessaryGString + fixedContent.contains("def semi = 'semicolon'") // UnnecessarySemicolon + !fixedContent.contains(";") + fixedContent.count('\n\n') == 3 // ConsecutiveBlankLines + } + + def "test codenarcFix task does not break strings with single quotes"() { + given: "a file with double quoted strings containing single quotes" + groovyFile.text = """package org.test + +class Test { + def s1 = "it's a test" + def s2 = "format 'yyyy-MM-dd'" + def s3 = "contains \\"double\\" and 'single' quotes" +} +""" + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "strings with single quotes are NOT changed to single quotes (which would break them)" + def content = groovyFile.text + content.contains('def s1 = "it\'s a test"') + content.contains('def s2 = "format \'yyyy-MM-dd\'"') + // s3 has escaped double quotes, so it should also remain double quoted + content.contains('def s3 = "contains \\"double\\" and \'single\' quotes"') + } + + def "test codenarcFix task does not break escaped double quotes in double quotes"() { + given: "a file with escaped double quotes" + groovyFile.text = """package org.test + +class Test { + def s = "\\"\\\$it\\"" +} +""" + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "escaped quotes are NOT broken" + groovyFile.text.contains('"\\"\\$it\\""') + } + + def "test codenarcFix task does not corrupt method references (::)"() { + given: "a file with :: method references" + groovyFile.text = """package org.test + +import java.util.Arrays + +class Test { + def result = Arrays.stream(items).map(String::trim).toList() + def other = items.collect(String::valueOf) + def cors = parseConfigList(config, 'allowedOrigins', corsConfig::setAllowedOrigins) +} +""" + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "method references are NOT broken" + def content = groovyFile.text + content.contains('String::trim') + content.contains('String::valueOf') + content.contains('corsConfig::setAllowedOrigins') + !content.contains('String: :trim') + !content.contains('String: :valueOf') + !content.contains('corsConfig: :setAllowedOrigins') + } + + def "test codenarcFix task does not corrupt adjacent GString and plain string"() { + given: "a file with a GString method name followed by a plain string argument" + groovyFile.text = '''package org.test + +class Test { + def invoke(invoker, name) { + invoker."${name}"("plain arg") + invoker."${name}"("-Pargs=${someVar}") + } +} +''' + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "adjacent GString boundaries are NOT fused" + def content = groovyFile.text + content.contains('invoker."${name}"(\'plain arg\')') + content.contains('invoker."${name}"("-Pargs=${someVar}")') + !content.contains('invoker."${name}\'(\'plain arg")') + !content.contains('invoker."${name}\'(\'plain arg\')') + } + + def "test codenarcFix task does not corrupt double quotes inside single-quoted strings"() { + given: "a file with single-quoted strings containing double quotes" + groovyFile.text = """package org.test + +class Test { + def d1 = description('Accepts format "hh:mm:ss"') + def d2 = writeLine('[cols="2,5,2", options="header"]') + def d3 = 'value with "double quotes" inside' +} +""" + when: "running codenarcFix" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('codenarcFix', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task finished successfully" + result.task(':codenarcFix').outcome == TaskOutcome.SUCCESS + + and: "double quotes inside single-quoted strings are NOT changed" + def content = groovyFile.text + content.contains('\'Accepts format "hh:mm:ss"\'') + content.contains('\'[cols="2,5,2", options="header"]\'') + content.contains('\'value with "double quotes" inside\'') + } +} diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy new file mode 100644 index 00000000000..beb8d66908e --- /dev/null +++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.buildsrc + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.file.Path + +class GrailsViolationAggregationPluginSpec extends Specification { + + @TempDir + Path testProjectDir + + def "plugin must be applied to root project only"() { + given: "a subproject-only build with the aggregation plugin" + testProjectDir.resolve('settings.gradle').toFile().text = "include 'sub'" + testProjectDir.resolve('build.gradle').toFile().text = '' + def sub = testProjectDir.resolve('sub') + sub.toFile().mkdirs() + sub.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + + when: "configuring the project" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('tasks') + .withPluginClasspath() + .buildAndFail() + + then: "an error is thrown" + result.output.contains('must be applied to the root project only') + } + + def "all aggregation tasks are registered on root"() { + given: "root project with aggregation plugin" + testProjectDir.resolve('settings.gradle').toFile().text = '' + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + + when: "listing verification tasks" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('tasks', '--group=verification') + .withPluginClasspath() + .build() + + then: + result.output.contains('aggregateStyleViolations') + result.output.contains('aggregateAnalysisViolations') + result.output.contains('aggregateViolations') + result.output.contains('aggregateJacocoCoverage') + } + + def "aggregateStyleViolations writes CodeNarc and Checkstyle reports to build/reports/violations/"() { + given: "a root project with a subproject that has codestyle XML reports" + testProjectDir.resolve('settings.gradle').toFile().text = "include 'app-module'" + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + def moduleDir = testProjectDir.resolve('app-module') + moduleDir.toFile().mkdirs() + moduleDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'groovy' + id 'org.apache.grails.gradle.grails-code-style' + } + repositories { mavenCentral() } + dependencies { + implementation localGroovy() + } + """ + def srcFile = moduleDir.resolve('src/main/groovy/com/example/AppClass.groovy').toFile() + srcFile.parentFile.mkdirs() + srcFile.text = 'package com.example\nclass AppClass {}' + + // Pre-populate XML reports in the standard consolidated location (build/reports/code-style/) + def checkstyleDir = testProjectDir.resolve('build/reports/code-style/checkstyle').toFile() + checkstyleDir.mkdirs() + new File(checkstyleDir, 'app-module-checkstyleMain.xml').text = """ + + + + + +""" + def codenarcDir = testProjectDir.resolve('build/reports/code-style/codenarc').toFile() + codenarcDir.mkdirs() + new File(codenarcDir, 'app-module-codenarcMain.xml').text = """ + + + + +The class is empty + + + + +""" + + when: "running aggregateStyleViolations skipping the actual style tasks" + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('aggregateStyleViolations', '-x', 'checkstyleMain', '-x', 'codenarcMain', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task succeeds" + result.task(':aggregateStyleViolations').outcome == TaskOutcome.SUCCESS + + and: "reports land in build/reports/violations/ — NOT in the repo root" + def violationsDir = testProjectDir.resolve('build/reports/violations').toFile() + new File(violationsDir, 'CHECKSTYLE_VIOLATIONS.md').exists() + new File(violationsDir, 'CODENARC_VIOLATIONS.md').exists() + !testProjectDir.resolve('CHECKSTYLE_VIOLATIONS.md').toFile().exists() + !testProjectDir.resolve('CODENARC_VIOLATIONS.md').toFile().exists() + + and: "checkstyle report contains the violation" + def checkstyleMd = new File(violationsDir, 'CHECKSTYLE_VIOLATIONS.md').text + checkstyleMd.contains('## Module: app-module') + checkstyleMd.contains('JavadocPackageCheck') + + and: "codenarc report contains the violation" + def codenarcMd = new File(violationsDir, 'CODENARC_VIOLATIONS.md').text + codenarcMd.contains('## Module: app-module') + codenarcMd.contains('EmptyClass') + } + + def "aggregateJacocoCoverage handles no csv reports gracefully"() { + given: "root project with no subproject csv reports" + testProjectDir.resolve('settings.gradle').toFile().text = '' + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('aggregateJacocoCoverage', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task succeeds without error" + result.task(':aggregateJacocoCoverage').outcome == TaskOutcome.SUCCESS + + and: "no report file is created" + !testProjectDir.resolve('build/reports/violations/JACOCO_COVERAGE.md').toFile().exists() + } + + def "aggregateJacocoCoverage excludes the default hibernate7 support classes"() { + given: "a root project with a jacoco csv containing an h7 support class and a normal class" + testProjectDir.resolve('settings.gradle').toFile().text = '' + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + writeJacocoCsv([ + 'app,org.grails.orm.hibernate.support.hibernate7,HibernateSupport,10,0', + 'app,org.example.kept,KeptClass,0,20', + ]) + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('aggregateJacocoCoverage', '--stacktrace') + .withPluginClasspath() + .build() + + then: "task succeeds and the report drops the colliding h7 class but keeps the normal one" + result.task(':aggregateJacocoCoverage').outcome == TaskOutcome.SUCCESS + def report = testProjectDir.resolve('build/reports/violations/JACOCO_COVERAGE.md').toFile() + report.exists() + def text = report.text + text.contains('org.example.kept.KeptClass') + !text.contains('org.grails.orm.hibernate.support.hibernate7.HibernateSupport') + } + + def "aggregateJacocoCoverage exclusion prefixes are configurable via property"() { + given: "a root project and a custom exclusion prefix that keeps the h7 class and drops a custom one" + testProjectDir.resolve('settings.gradle').toFile().text = '' + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' + } + """ + writeJacocoCsv([ + 'app,org.grails.orm.hibernate.support.hibernate7,HibernateSupport,10,0', + 'app,com.example.skip,SkipMe,5,5', + 'app,org.example.kept,KeptClass,0,20', + ]) + + when: + def result = GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments('aggregateJacocoCoverage', '-Pgrails.jacoco.aggregation.excludedClassPrefixes=com.example.skip', '--stacktrace') + .withPluginClasspath() + .build() + + then: "the custom prefix is dropped while the default h7 class is now retained" + result.task(':aggregateJacocoCoverage').outcome == TaskOutcome.SUCCESS + def text = testProjectDir.resolve('build/reports/violations/JACOCO_COVERAGE.md').toFile().text + text.contains('org.example.kept.KeptClass') + text.contains('org.grails.orm.hibernate.support.hibernate7.HibernateSupport') + !text.contains('com.example.skip.SkipMe') + } + + private void writeJacocoCsv(List dataRows) { + def csv = testProjectDir.resolve('build/reports/jacoco/test/jacocoTestReport.csv').toFile() + csv.parentFile.mkdirs() + csv.text = (['GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED'] + dataRows).join('\n') + '\n' + } +} diff --git a/build.gradle b/build.gradle index bb330e24872..87b54c25f29 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,10 @@ * limitations under the License. */ +plugins { + id 'org.apache.grails.gradle.grails-violation-aggregation' +} + import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset diff --git a/gradle.properties b/gradle.properties index 1c26a51a967..77c27d353b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -70,6 +70,8 @@ jbossTransactionApiVersion=2.0.0.Final # build dependencies for code quality checks checkstyleVersion=11.0.0 codenarcVersion=3.6.0-groovy-4.0 +pmdVersion=7.25.0 +spotbugsPluginVersion=6.4.8 # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs # https://github.com/apache/grails-gradle-plugin/issues/222 diff --git a/gradlew.bat b/gradlew.bat index aa5f10b069f..24c62d56f2d 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/grails-gradle/build.gradle b/grails-gradle/build.gradle index a8959b364db..382c9771024 100644 --- a/grails-gradle/build.gradle +++ b/grails-gradle/build.gradle @@ -23,6 +23,7 @@ import java.time.format.DateTimeFormatter plugins { id 'org.apache.grails.buildsrc.properties' id 'org.apache.grails.buildsrc.dependency-validator' + id 'org.apache.grails.gradle.grails-violation-aggregation' } allprojects { diff --git a/grails-gradle/gradlew.bat b/grails-gradle/gradlew.bat index aa5f10b069f..24c62d56f2d 100755 --- a/grails-gradle/gradlew.bat +++ b/grails-gradle/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/grails-profiles/profile/skeleton/gradlew.bat b/grails-profiles/profile/skeleton/gradlew.bat index aa5f10b069f..24c62d56f2d 100755 --- a/grails-profiles/profile/skeleton/gradlew.bat +++ b/grails-profiles/profile/skeleton/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/grails-shell-cli/src/test/resources/gradle-sample/gradlew.bat b/grails-shell-cli/src/test/resources/gradle-sample/gradlew.bat index aa5f10b069f..24c62d56f2d 100644 --- a/grails-shell-cli/src/test/resources/gradle-sample/gradlew.bat +++ b/grails-shell-cli/src/test/resources/gradle-sample/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@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 -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL%