This is an automated email from the ASF dual-hosted git repository.

GGraziadei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git


The following commit(s) were added to refs/heads/master by this push:
     new be45c93b4 Add checkstyle auto-fix tooling (#9017)
be45c93b4 is described below

commit be45c93b48f8ad05a06bb12f2af68dc5b944b5a1
Author: sergio <[email protected]>
AuthorDate: Sun Oct 18 15:20:51 2026 +0200

    Add checkstyle auto-fix tooling (#9017)
    
    * Add OpenRewrite + Groovy checkstyle auto-fix tooling (run on demand)
    
    OpenRewrite is preferred: everything it has a recipe for (fully or partly) 
runs
    there, driven by a style in rewrite.yml that mirrors storm_checkstyle.xml. 
The
    Groovy script only keeps the rules with no recipe (LineLength, 
OneStatementPerLine,
    SeparatorWrap, Javadoc rules, member ordering, trivial text rules). A second
    OpenRewrite execution re-indents after the Groovy stage. A Groovy script 
normalizes
    license headers to the ASF template, leaving third-party works alone.
    
    Wired into the root and module poms without binding it to validate.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
    
    * Add a workflow_dispatch GitHub Action to run the checkstyle auto-fix per 
module
    
    Takes the module as input, runs OpenRewrite, the Groovy stage and the 
re-indent
    pass plus the license header normalization on it, and pushes the result to
    checkstyle-fix/<module> so one pull request per module can be opened 
against master.
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 .github/workflows/checkstyle-autofix.yaml          |  121 ++
 .mvn/.gitkeep                                      |    0
 .mvn/jvm.config                                    |   11 +
 dev-tools/gitact/checkstyle-report.sh              |   61 +
 examples/storm-hdfs-examples/pom.xml               |   10 +
 examples/storm-jdbc-examples/pom.xml               |   10 +
 examples/storm-jms-examples/pom.xml                |   10 +
 examples/storm-kafka-client-examples/pom.xml       |   10 +
 examples/storm-loadgen/pom.xml                     |   10 +
 examples/storm-perf/pom.xml                        |   10 +
 examples/storm-redis-examples/pom.xml              |   10 +
 examples/storm-starter/pom.xml                     |   10 +
 external/storm-autocreds/pom.xml                   |   10 +
 external/storm-blobstore-migration/pom.xml         |   10 +
 external/storm-hdfs-blobstore/pom.xml              |   10 +
 external/storm-hdfs-oci/pom.xml                    |   10 +
 external/storm-hdfs/pom.xml                        |   10 +
 external/storm-jdbc/pom.xml                        |   10 +
 external/storm-jms/pom.xml                         |   10 +
 external/storm-kafka-client/pom.xml                |   10 +
 external/storm-kafka-migration/pom.xml             |   10 +
 external/storm-kafka-monitor/pom.xml               |   10 +
 external/storm-metrics/pom.xml                     |   10 +
 external/storm-redis/pom.xml                       |   10 +
 flux/flux-core/pom.xml                             |   10 +
 flux/flux-examples/pom.xml                         |   10 +
 flux/flux-wrappers/pom.xml                         |   10 +
 flux/pom.xml                                       |   10 +
 integration-test/pom.xml                           |   10 +
 pom.xml                                            |  144 +-
 rewrite.yml                                        |  202 +++
 storm-checkstyle/README.md                         |  144 ++
 .../main/resources/storm/checkstyle-autofix.groovy | 1633 ++++++++++++++++++++
 .../storm/normalize-license-headers.groovy         |  176 +++
 .../src/main/resources/storm/storm_checkstyle.xml  |    9 +
 storm-client/pom.xml                               |   10 +
 storm-core/pom.xml                                 |   10 +
 storm-multilang/javascript/pom.xml                 |   10 +
 storm-multilang/ruby/pom.xml                       |   10 +
 storm-server/pom.xml                               |   10 +
 storm-submit-tools/pom.xml                         |   10 +
 storm-webapp/pom.xml                               |   10 +
 42 files changed, 2818 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/checkstyle-autofix.yaml 
b/.github/workflows/checkstyle-autofix.yaml
new file mode 100644
index 000000000..267ff585a
--- /dev/null
+++ b/.github/workflows/checkstyle-autofix.yaml
@@ -0,0 +1,121 @@
+# 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
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Runs the checkstyle auto-fix (storm-checkstyle/README.md) on one module, 
pushes the result to a
+# new branch checkstyle-fix/<module>-<run id>-<attempt> (one per execution) 
and opens a pull request
+# from it against the branch the workflow ran on. The Checkstyle report of the 
module before and
+# after the fix is attached to the run as an artifact; the violations left 
after the fix are listed
+# in the pull request.
+name: Checkstyle auto-fix
+
+on:
+  workflow_dispatch:
+    inputs:
+      module:
+        description: 'Maven module to fix, as its path in the repository (e.g. 
storm-client or external/storm-hdfs)'
+        required: true
+        type: string
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  autofix:
+    runs-on: ubuntu-latest
+    timeout-minutes: 120
+    env:
+      MODULE: ${{ inputs.module }}
+    steps:
+      - name: Validate the module input
+        run: |
+          if ! [[ "$MODULE" =~ ^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$ ]] || [[ 
"$MODULE" == *..* ]]; then
+            echo "::error::'$MODULE' is not a valid module path"
+            exit 1
+          fi
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
+      - name: Check that the module exists
+        run: test -f "$MODULE/pom.xml" || { echo "::error::$MODULE/pom.xml not 
found"; exit 1; }
+      - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+        with:
+          path: ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+      - name: Set up JDK 25
+        uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # 
v6.0.1
+        with:
+          distribution: temurin
+          java-version: 25
+      - name: Check that the module declares the auto-fix plugins
+        run: |
+          # without them the auto-fix executions would run with no 
configuration and silently fix nothing
+          mvn --batch-mode --quiet -pl "$MODULE" help:effective-pom 
-Doutput="$RUNNER_TEMP/effective-pom.xml"
+          python3 - "$RUNNER_TEMP/effective-pom.xml" <<'EOF'
+          import sys
+          import xml.etree.ElementTree as ET
+          ns = {'m': 'http://maven.apache.org/POM/4.0.0'}
+          plugins = {p.text for p in 
ET.parse(sys.argv[1]).getroot().findall('m:build/m:plugins/m:plugin/m:artifactId',
 ns)}
+          missing = {'rewrite-maven-plugin', 'gmavenplus-plugin'} - plugins
+          if missing:
+              sys.exit(f"::error::the module does not declare {', 
'.join(sorted(missing))} in its build plugins")
+          EOF
+      - name: Build the module and the modules it depends on (the Groovy stage 
resolves its classpath)
+        run: mvn --batch-mode install -DskipTests -Dcheckstyle.skip=true -pl 
"$MODULE" -am
+      - name: Checkstyle report before the fix
+        run: ./dev-tools/gitact/checkstyle-report.sh before
+      - name: Auto-fix the module (OpenRewrite, Groovy, OpenRewrite re-indent)
+        run: |
+          mvn --batch-mode -pl "$MODULE" -Dcheckstyle.skip=true \
+              
org.openrewrite.maven:rewrite-maven-plugin:run@checkstyle-autofix-openrewrite \
+              
org.codehaus.gmavenplus:gmavenplus-plugin:execute@checkstyle-autofix \
+              
org.openrewrite.maven:rewrite-maven-plugin:run@checkstyle-reindent-openrewrite
+      - name: Normalize the license headers
+        run: mvn --batch-mode -pl "$MODULE" -Dcheckstyle.skip=true 
org.codehaus.gmavenplus:gmavenplus-plugin:execute@normalize-license-headers
+      - name: Checkstyle report after the fix
+        run: ./dev-tools/gitact/checkstyle-report.sh after
+      - name: Upload the Checkstyle reports
+        if: ${{ !cancelled() }}
+        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
# v7.0.1
+        with:
+          name: checkstyle-reports
+          path: ${{ runner.temp }}/checkstyle-reports/
+          if-no-files-found: ignore
+      - name: Push the result to a new branch and open a pull request
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          # one branch per execution, so a run never overwrites the branch of 
an earlier one (or its open PR)
+          BRANCH="checkstyle-fix/$MODULE-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
+          # only the module itself (build output is git-ignored), never the 
tooling files
+          git add -A -- "$MODULE"
+          if git diff --cached --quiet; then
+            echo "Nothing to fix in $MODULE" | tee -a "$GITHUB_STEP_SUMMARY"
+            exit 0
+          fi
+          git config user.name "github-actions[bot]"
+          git config user.email 
"41898282+github-actions[bot]@users.noreply.github.com"
+          git checkout -b "$BRANCH"
+          git commit -m "Checkstyle auto-fix of $MODULE"
+          git push origin "$BRANCH"
+          REPORTS="$RUNNER_TEMP/checkstyle-reports"
+          {
+            echo "Checkstyle auto-fix of \`$MODULE\`, made by [this workflow 
run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID) (the 
full reports are in its \`checkstyle-reports\` artifact)."
+            echo
+            cat "$REPORTS/before.md" "$REPORTS/after.md"
+          } > "$RUNNER_TEMP/pr-body.md"
+          PR=$(gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" \
+              --title "Checkstyle auto-fix of $MODULE" --body-file 
"$RUNNER_TEMP/pr-body.md")
+          echo "Opened $PR from $BRANCH" | tee -a "$GITHUB_STEP_SUMMARY"
diff --git a/.mvn/.gitkeep b/.mvn/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 000000000..3d61a1f27
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1,11 @@
+--add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
diff --git a/dev-tools/gitact/checkstyle-report.sh 
b/dev-tools/gitact/checkstyle-report.sh
new file mode 100755
index 000000000..358dad416
--- /dev/null
+++ b/dev-tools/gitact/checkstyle-report.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# 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
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Runs the build's Checkstyle check on $MODULE and saves its report as
+# $RUNNER_TEMP/checkstyle-reports/<label>.xml (and the console output as 
<label>.txt), plus
+# <label>.md: the violation counts and a table of the violations per check, 
which is also added to
+# the job summary. Used by .github/workflows/checkstyle-autofix.yaml.
+# The check failing on violations is expected: only a missing report fails 
this script.
+
+set -u
+
+LABEL=$1
+OUT_DIR="$RUNNER_TEMP/checkstyle-reports"
+REPORT="$MODULE/target/checkstyle-violation.xml"
+mkdir -p "$OUT_DIR"
+rm -f "$REPORT"
+
+mvn --batch-mode -pl "$MODULE" 
org.apache.maven.plugins:maven-checkstyle-plugin:check@validate \
+    > "$OUT_DIR/$LABEL.txt" 2>&1
+
+if [[ ! -f "$REPORT" ]]; then
+    tail -n 50 "$OUT_DIR/$LABEL.txt"
+    echo "::error::Checkstyle did not write $REPORT"
+    exit 1
+fi
+cp "$REPORT" "$OUT_DIR/$LABEL.xml"
+
+python3 - "$REPORT" "$LABEL" > "$OUT_DIR/$LABEL.md" <<'EOF'
+import collections
+import sys
+import xml.etree.ElementTree as ET
+
+report, label = sys.argv[1:]
+violations = list(ET.parse(report).getroot().iter('error'))
+severities = collections.Counter(v.get('severity') for v in violations)
+print(f"### Checkstyle {label} the fix: {severities['error']} errors, 
{severities['warning']} warnings")
+if violations:
+    print()
+    print('| Check | Severity | Violations |')
+    print('|---|---|---|')
+    checks = collections.Counter((v.get('source').rsplit('.', 
1)[-1].removesuffix('Check'), v.get('severity'))
+                                 for v in violations)
+    for (check, severity), count in checks.most_common():
+        print(f'| {check} | {severity} | {count} |')
+print()
+EOF
+cat "$OUT_DIR/$LABEL.md"
+cat "$OUT_DIR/$LABEL.md" >> "$GITHUB_STEP_SUMMARY"
diff --git a/examples/storm-hdfs-examples/pom.xml 
b/examples/storm-hdfs-examples/pom.xml
index 40e66a2a5..a307844ef 100644
--- a/examples/storm-hdfs-examples/pom.xml
+++ b/examples/storm-hdfs-examples/pom.xml
@@ -93,6 +93,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-jdbc-examples/pom.xml 
b/examples/storm-jdbc-examples/pom.xml
index 95525a3cc..5a3e7ab82 100644
--- a/examples/storm-jdbc-examples/pom.xml
+++ b/examples/storm-jdbc-examples/pom.xml
@@ -85,6 +85,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-jms-examples/pom.xml 
b/examples/storm-jms-examples/pom.xml
index 41cf4b9bd..47f765b7f 100644
--- a/examples/storm-jms-examples/pom.xml
+++ b/examples/storm-jms-examples/pom.xml
@@ -105,6 +105,16 @@
                 </executions>
 
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-kafka-client-examples/pom.xml 
b/examples/storm-kafka-client-examples/pom.xml
index 4700be771..f29819951 100644
--- a/examples/storm-kafka-client-examples/pom.xml
+++ b/examples/storm-kafka-client-examples/pom.xml
@@ -103,6 +103,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-loadgen/pom.xml b/examples/storm-loadgen/pom.xml
index 0ae6a3b99..16080974f 100644
--- a/examples/storm-loadgen/pom.xml
+++ b/examples/storm-loadgen/pom.xml
@@ -122,6 +122,16 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+          <groupId>org.openrewrite.maven</groupId>
+          <artifactId>rewrite-maven-plugin</artifactId>
+          <!--Note - the version/config would be inherited-->
+      </plugin>
+      <plugin>
+          <groupId>org.codehaus.gmavenplus</groupId>
+          <artifactId>gmavenplus-plugin</artifactId>
+          <!--Note - the version/config would be inherited-->
+      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-perf/pom.xml b/examples/storm-perf/pom.xml
index 7e9377cbf..69c435b9e 100644
--- a/examples/storm-perf/pom.xml
+++ b/examples/storm-perf/pom.xml
@@ -86,6 +86,16 @@
                     <mainClass>${storm.topology}</mainClass>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-redis-examples/pom.xml 
b/examples/storm-redis-examples/pom.xml
index 68db938f6..662f5e2a3 100644
--- a/examples/storm-redis-examples/pom.xml
+++ b/examples/storm-redis-examples/pom.xml
@@ -89,6 +89,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/examples/storm-starter/pom.xml b/examples/storm-starter/pom.xml
index f7632397f..b6dc1e66c 100644
--- a/examples/storm-starter/pom.xml
+++ b/examples/storm-starter/pom.xml
@@ -174,6 +174,16 @@
                     <excludedGroups>none</excludedGroups>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-autocreds/pom.xml b/external/storm-autocreds/pom.xml
index 80ecdc4f1..bdf1c3cff 100644
--- a/external/storm-autocreds/pom.xml
+++ b/external/storm-autocreds/pom.xml
@@ -112,6 +112,16 @@
     </dependencies>
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-blobstore-migration/pom.xml 
b/external/storm-blobstore-migration/pom.xml
index ae3bfa3b8..9150aa0a2 100644
--- a/external/storm-blobstore-migration/pom.xml
+++ b/external/storm-blobstore-migration/pom.xml
@@ -146,6 +146,16 @@ limitations under the License.
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-hdfs-blobstore/pom.xml 
b/external/storm-hdfs-blobstore/pom.xml
index a33bbca96..fe31d7fab 100644
--- a/external/storm-hdfs-blobstore/pom.xml
+++ b/external/storm-hdfs-blobstore/pom.xml
@@ -153,6 +153,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-hdfs-oci/pom.xml b/external/storm-hdfs-oci/pom.xml
index 73b3f0c95..e0d4990a9 100644
--- a/external/storm-hdfs-oci/pom.xml
+++ b/external/storm-hdfs-oci/pom.xml
@@ -104,6 +104,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-hdfs/pom.xml b/external/storm-hdfs/pom.xml
index ca1207cfb..565ddd0e6 100644
--- a/external/storm-hdfs/pom.xml
+++ b/external/storm-hdfs/pom.xml
@@ -145,6 +145,16 @@
                     <forkCount>1</forkCount>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-jdbc/pom.xml b/external/storm-jdbc/pom.xml
index 770754154..4816832ef 100644
--- a/external/storm-jdbc/pom.xml
+++ b/external/storm-jdbc/pom.xml
@@ -77,6 +77,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-jms/pom.xml b/external/storm-jms/pom.xml
index 5aa3fc2ab..dfd61417e 100644
--- a/external/storm-jms/pom.xml
+++ b/external/storm-jms/pom.xml
@@ -80,6 +80,16 @@
                     <additionalparam>-Xdoclint:none</additionalparam>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-kafka-client/pom.xml 
b/external/storm-kafka-client/pom.xml
index 368c3dce5..b47b75e3a 100644
--- a/external/storm-kafka-client/pom.xml
+++ b/external/storm-kafka-client/pom.xml
@@ -173,6 +173,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-kafka-migration/pom.xml 
b/external/storm-kafka-migration/pom.xml
index 1a0f1e580..f2bcde4ba 100644
--- a/external/storm-kafka-migration/pom.xml
+++ b/external/storm-kafka-migration/pom.xml
@@ -81,6 +81,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-kafka-monitor/pom.xml 
b/external/storm-kafka-monitor/pom.xml
index 326e00a34..0aa34b473 100644
--- a/external/storm-kafka-monitor/pom.xml
+++ b/external/storm-kafka-monitor/pom.xml
@@ -81,6 +81,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-metrics/pom.xml b/external/storm-metrics/pom.xml
index 31197865b..da14dbce2 100644
--- a/external/storm-metrics/pom.xml
+++ b/external/storm-metrics/pom.xml
@@ -99,6 +99,16 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+          <groupId>org.openrewrite.maven</groupId>
+          <artifactId>rewrite-maven-plugin</artifactId>
+          <!--Note - the version/config would be inherited-->
+      </plugin>
+      <plugin>
+          <groupId>org.codehaus.gmavenplus</groupId>
+          <artifactId>gmavenplus-plugin</artifactId>
+          <!--Note - the version/config would be inherited-->
+      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/external/storm-redis/pom.xml b/external/storm-redis/pom.xml
index 0c240cea9..9a380b24b 100644
--- a/external/storm-redis/pom.xml
+++ b/external/storm-redis/pom.xml
@@ -100,6 +100,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/flux/flux-core/pom.xml b/flux/flux-core/pom.xml
index 3f5b9957a..2fcb43fa4 100644
--- a/flux/flux-core/pom.xml
+++ b/flux/flux-core/pom.xml
@@ -114,6 +114,16 @@
                 </execution>
             </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/flux/flux-examples/pom.xml b/flux/flux-examples/pom.xml
index 6cfaa7e3f..bc3746189 100644
--- a/flux/flux-examples/pom.xml
+++ b/flux/flux-examples/pom.xml
@@ -121,6 +121,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/flux/flux-wrappers/pom.xml b/flux/flux-wrappers/pom.xml
index 0b69c5672..635daa788 100644
--- a/flux/flux-wrappers/pom.xml
+++ b/flux/flux-wrappers/pom.xml
@@ -52,6 +52,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/flux/pom.xml b/flux/pom.xml
index fc1712573..098290f12 100644
--- a/flux/pom.xml
+++ b/flux/pom.xml
@@ -60,6 +60,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/integration-test/pom.xml b/integration-test/pom.xml
index f9295f1e7..922a6332f 100644
--- a/integration-test/pom.xml
+++ b/integration-test/pom.xml
@@ -163,6 +163,16 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/pom.xml b/pom.xml
index 4bb39882c..7fc37e9e4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,15 @@
 
     <properties>
         <javaVersion>25</javaVersion>
+        <!-- checkstyle auto-fix tooling (run on demand, see 
storm-checkstyle/README.md) -->
+        <rewrite-maven-plugin.version>6.46.1</rewrite-maven-plugin.version>
+        
<rewrite-static-analysis.version>2.41.1</rewrite-static-analysis.version>
+        <gmavenplus.version>5.1.0</gmavenplus.version>
+        <groovy.version>4.0.33</groovy.version>
+        <!-- Used by both the checkstyle check and checkstyle-autofix.groovy. 
If you change this, you should
+        also update the storm_checkstyle.xml file to be based on the 
google_checks.xml from the version
+        of checkstyle you are choosing. -->
+        <checkstyle.version>14.3.0</checkstyle.version>
         <maven.javadoc.failOnError>false</maven.javadoc.failOnError>
         <maven.javadoc.failOnWarnings>false</maven.javadoc.failOnWarnings>
 
@@ -1113,6 +1122,137 @@
                         </execution>
                     </executions>
                 </plugin>
+                <plugin>
+                    <!--
+                    OpenRewrite stages of the checkstyle auto-fix (see 
storm-checkstyle/README.md). Not bound to
+                    any phase: run on demand. The recipes and the style 
mirroring storm_checkstyle.xml are in
+                    rewrite.yml at the repo root. Thrift-generated code and 
the Groovy scripts of
+                    storm-checkstyle are not checked by Checkstyle, so they 
are left alone.
+                    -->
+                    <groupId>org.openrewrite.maven</groupId>
+                    <artifactId>rewrite-maven-plugin</artifactId>
+                    <version>${rewrite-maven-plugin.version}</version>
+                    <configuration>
+                        
<configLocation>${maven.multiModuleProjectDirectory}/rewrite.yml</configLocation>
+                        <activeStyles>
+                            <style>org.apache.storm.checkstyle.Style</style>
+                        </activeStyles>
+                        <exclusions>
+                            <exclusion>**/generated/**</exclusion>
+                            <exclusion>**/resources/storm/*.groovy</exclusion>
+                        </exclusions>
+                        <!-- Styles derived from the ruleset conflict with it 
(RightCurly 'alone' would push
+                        catch/else onto their own line); the explicit style in 
rewrite.yml is used instead. -->
+                        
<checkstyleDetectionEnabled>false</checkstyleDetectionEnabled>
+                        <exportDatatables>false</exportDatatables>
+                    </configuration>
+                    <dependencies>
+                        <dependency>
+                            <groupId>org.openrewrite.recipe</groupId>
+                            <artifactId>rewrite-static-analysis</artifactId>
+                            
<version>${rewrite-static-analysis.version}</version>
+                        </dependency>
+                    </dependencies>
+                    <executions>
+                        <execution>
+                            <!-- first pass, before checkstyle-autofix.groovy 
-->
+                            <id>checkstyle-autofix-openrewrite</id>
+                            <phase>none</phase>
+                            <goals>
+                                <goal>run</goal>
+                            </goals>
+                            <configuration>
+                                <activeRecipes>
+                                    
<recipe>org.apache.storm.checkstyle.AutoFix</recipe>
+                                </activeRecipes>
+                            </configuration>
+                        </execution>
+                        <execution>
+                            <!-- re-indent pass, after 
checkstyle-autofix.groovy -->
+                            <id>checkstyle-reindent-openrewrite</id>
+                            <phase>none</phase>
+                            <goals>
+                                <goal>run</goal>
+                            </goals>
+                            <configuration>
+                                <activeRecipes>
+                                    
<recipe>org.apache.storm.checkstyle.Reindent</recipe>
+                                </activeRecipes>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
+                    <!--
+                    Groovy stages of the checkstyle auto-fix and license 
header normalization (see
+                    storm-checkstyle/README.md). Not bound to any phase: run 
on demand. gmavenplus interprets
+                    the scripts directly, so they need no prior `compile` 
phase.
+                    -->
+                    <groupId>org.codehaus.gmavenplus</groupId>
+                    <artifactId>gmavenplus-plugin</artifactId>
+                    <version>${gmavenplus.version}</version>
+                    <dependencies>
+                        <dependency>
+                            <groupId>org.apache.groovy</groupId>
+                            <artifactId>groovy</artifactId>
+                            <version>${groovy.version}</version>
+                        </dependency>
+                        <dependency>
+                            <!-- XmlSlurper, used by checkstyle-autofix.groovy 
to parse the
+                            checkstyle ruleset itself - split out of Groovy 
core since Groovy 4. -->
+                            <groupId>org.apache.groovy</groupId>
+                            <artifactId>groovy-xml</artifactId>
+                            <version>${groovy.version}</version>
+                        </dependency>
+                        <dependency>
+                            <!-- checkstyle-autofix.groovy runs Checkstyle 
itself to find what to fix, so it
+                            must be the same version the checkstyle check 
uses. -->
+                            <groupId>com.puppycrawl.tools</groupId>
+                            <artifactId>checkstyle</artifactId>
+                            <version>${checkstyle.version}</version>
+                            <exclusions>
+                                <!-- only needed for Checkstyle's site/XDoc 
output, not to run the checker -->
+                                <exclusion>
+                                    <groupId>org.apache.maven.doxia</groupId>
+                                    <artifactId>*</artifactId>
+                                </exclusion>
+                            </exclusions>
+                        </dependency>
+                        <dependency>
+                            <!-- needed at run time by Checkstyle 
(commons-beanutils) -->
+                            <groupId>commons-logging</groupId>
+                            <artifactId>commons-logging</artifactId>
+                            <version>${commons-logging.version}</version>
+                        </dependency>
+                    </dependencies>
+                    <executions>
+                        <execution>
+                            <id>checkstyle-autofix</id>
+                            <phase>none</phase>
+                            <goals>
+                                <goal>execute</goal>
+                            </goals>
+                            <configuration>
+                                <scripts>
+                                    
<script>file:///${maven.multiModuleProjectDirectory}/storm-checkstyle/src/main/resources/storm/checkstyle-autofix.groovy</script>
+                                </scripts>
+                            </configuration>
+                        </execution>
+                        <execution>
+                            <!-- add -Dlicense.check=true to only report -->
+                            <id>normalize-license-headers</id>
+                            <phase>none</phase>
+                            <goals>
+                                <goal>execute</goal>
+                            </goals>
+                            <configuration>
+                                <scripts>
+                                    
<script>file:///${maven.multiModuleProjectDirectory}/storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy</script>
+                                </scripts>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
                 <plugin>
                     <!--To support checkstyle goals. For example: "mvn 
checkstyle:checkstyle"-->
                     <groupId>org.apache.maven.plugins</groupId>
@@ -1127,9 +1267,7 @@
                         <dependency>
                             <groupId>com.puppycrawl.tools</groupId>
                             <artifactId>checkstyle</artifactId>
-                            <!-- If you change this, you should also update 
the storm_checkstyle.xml file to be
-                            based on the google_checks.xml from the version of 
checkstyle you are choosing. -->
-                            <version>14.3.0</version>
+                            <version>${checkstyle.version}</version>
                         </dependency>
                     </dependencies>
                     <executions>
diff --git a/rewrite.yml b/rewrite.yml
new file mode 100644
index 000000000..727881b27
--- /dev/null
+++ b/rewrite.yml
@@ -0,0 +1,202 @@
+#
+# 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
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+---
+# First stage of the checkstyle auto-fix, run on demand by the 
rewrite-maven-plugin
+# execution `checkstyle-autofix-openrewrite` in the root pom (bound to no 
phase), just before
+# checkstyle-autofix.groovy (which then fixes what is left, text-level, driven 
by
+# Checkstyle's own reports).
+#
+# Everything OpenRewrite has a recipe for lives here; the Groovy stage keeps 
only the rules
+# with no recipe (see storm-checkstyle/README.md for the split). The 
formatting recipes need
+# a style, which is declared below (org.apache.storm.checkstyle.Style) and 
mirrors
+# storm_checkstyle.xml instead of being derived from it: the derived styles 
conflict with the
+# ruleset (RightCurly 'alone' would push catch/else onto their own line).
+#
+# Deliberately NOT here:
+#   - org.openrewrite.java.format.AutoFormat: bundles formatters that do not 
follow the ruleset
+#     and introduces star imports (AvoidStarImport).
+#   - org.openrewrite.staticanalysis.TypecastParenPad: turns `(T) x` into 
`(T)x`, which
+#     violates WhitespaceAfter (the Spaces recipe below covers ParenPad on its 
own).
+type: specs.openrewrite.org/v1beta/recipe
+name: org.apache.storm.checkstyle.AutoFix
+displayName: Fix storm_checkstyle.xml violations that need a parsed AST
+description: >
+  NeedBraces, MultipleVariableDeclarations, ModifierOrder, UpperEll, 
ArrayTypeStyle,
+  FileTabCharacter, OperatorWrap, WhitespaceAround/After, NoWhitespaceBefore, 
ParenPad,
+  MethodParamPad, GenericWhitespace, Indentation, EmptyLineSeparator, 
CustomImportOrder,
+  AvoidStarImport and MissingSwitchDefault (only a misplaced default).
+recipeList:
+  - org.openrewrite.staticanalysis.NeedBraces
+  - org.openrewrite.staticanalysis.MultipleVariableDeclarations
+  - org.openrewrite.staticanalysis.ModifierOrder
+  - org.openrewrite.staticanalysis.UpperCaseLiteralSuffixes
+  - org.openrewrite.staticanalysis.UseJavaStyleArrayDeclarations
+  # OperatorWrap (option NL in the ruleset)
+  - org.openrewrite.staticanalysis.OperatorWrap:
+      wrapOption: NL
+  # FileTabCharacter
+  - org.openrewrite.java.format.NormalizeTabsOrSpaces
+  # WhitespaceAround, WhitespaceAfter, NoWhitespaceBefore, ParenPad, 
MethodParamPad, GenericWhitespace
+  - org.openrewrite.java.format.Spaces
+  # LeftCurly, RightCurly, NoLineWrap, AnnotationLocation (partial: see 
storm-checkstyle/README.md)
+  - org.openrewrite.java.format.WrappingAndBraces
+  # Indentation, CommentsIndentation
+  - org.openrewrite.java.format.TabsAndIndents
+  # EmptyLineSeparator
+  - org.openrewrite.java.format.BlankLines
+  # AvoidStarImport: unfolds star imports to the types actually used (needs 
type attribution)
+  - org.openrewrite.java.RemoveUnusedImports
+  # MissingSwitchDefault: only moves an existing `default` last
+  - org.openrewrite.staticanalysis.DefaultComesLast
+  # CustomImportOrder (STATIC###THIRD_PARTY_PACKAGE, sorted, one blank line 
between the groups)
+  - org.openrewrite.java.OrderImports:
+      removeUnused: false
+---
+# Second pass, run after checkstyle-autofix.groovy: its LineLength wraps leave 
continuation lines
+# at the wrong indent. Only TabsAndIndents runs here; Spaces would turn the 
`{}` the Groovy stage
+# writes for empty blocks back into `{ }`.
+type: specs.openrewrite.org/v1beta/recipe
+name: org.apache.storm.checkstyle.Reindent
+displayName: Re-indent after the Groovy stage
+description: Indentation only.
+recipeList:
+  - org.openrewrite.java.format.TabsAndIndents
+---
+type: specs.openrewrite.org/v1beta/style
+name: org.apache.storm.checkstyle.Style
+displayName: Style mirroring storm_checkstyle.xml
+styleConfigs:
+  - org.openrewrite.java.style.TabsAndIndentsStyle:
+      useTabCharacter: false
+      tabSize: 4
+      indentSize: 4
+      continuationIndent: 8
+      indentsRelativeToExpressionStart: false
+  - org.openrewrite.java.style.BlankLinesStyle:
+      keepMaximum:
+        inDeclarations: 1
+        inCode: 1
+        beforeEndOfBlock: 1
+        betweenHeaderAndPackage: 1
+      minimum:
+        beforePackage: 0
+        afterPackage: 1
+        beforeImports: 1
+        afterImports: 1
+        aroundClass: 1
+        afterClassHeader: 0
+        beforeClassEnd: 0
+        afterAnonymousClassHeader: 0
+        aroundFieldInInterface: 0
+        aroundField: 0
+        aroundMethodInInterface: 1
+        aroundMethod: 1
+        beforeMethodBody: 0
+        aroundInitializer: 1
+  - org.openrewrite.java.style.ImportLayoutStyle:
+      # static imports first, then everything else; never fold into a star 
import
+      classCountToUseStarImport: 2147483647
+      nameCountToUseStarImport: 2147483647
+      layout:
+        - import static all other imports
+        - <blank line>
+        - import all other imports
+  - org.openrewrite.java.style.WrappingAndBracesStyle:
+      # RightCurly 'same' for else/catch/finally; annotations of types and 
methods on their own line
+      ifStatement:
+        elseOnNewLine: false
+        specialElseIfTreatment: true
+      tryStatement:
+        catchOnNewLine: false
+        finallyOnNewLine: false
+      classAnnotations:
+        wrap: WrapAlways
+      methodAnnotations:
+        wrap: WrapAlways
+  - org.openrewrite.java.style.SpacesStyle:
+      beforeParentheses:
+        methodDeclaration: false
+        methodCall: false
+        ifParentheses: true
+        forParentheses: true
+        whileParentheses: true
+        switchParentheses: true
+        tryParentheses: true
+        catchParentheses: true
+        synchronizedParentheses: true
+        annotationParameters: false
+      aroundOperators:
+        assignment: true
+        logical: true
+        equality: true
+        relational: true
+        bitwise: true
+        additive: true
+        multiplicative: true
+        shift: true
+        unary: false
+        lambdaArrow: true
+        methodReferenceDoubleColon: false
+      beforeLeftBrace:
+        classLeftBrace: true
+        methodLeftBrace: true
+        ifLeftBrace: true
+        elseLeftBrace: true
+        forLeftBrace: true
+        whileLeftBrace: true
+        doLeftBrace: true
+        switchLeftBrace: true
+        tryLeftBrace: true
+        catchLeftBrace: true
+        finallyLeftBrace: true
+        synchronizedLeftBrace: true
+        arrayInitializerLeftBrace: true
+        annotationArrayInitializerLeftBrace: false
+      within:
+        codeBraces: true
+        brackets: false
+        arrayInitializerBraces: false
+        emptyArrayInitializerBraces: false
+        groupingParentheses: false
+        methodDeclarationParentheses: false
+        emptyMethodDeclarationParentheses: false
+        methodCallParentheses: false
+        emptyMethodCallParentheses: false
+        ifParentheses: false
+        forParentheses: false
+        whileParentheses: false
+        switchParentheses: false
+        tryParentheses: false
+        catchParentheses: false
+        synchronizedParentheses: false
+        typeCastParentheses: false
+        annotationParentheses: false
+        angleBrackets: false
+        recordHeader: false
+      other:
+        beforeComma: false
+        afterComma: true
+        beforeForSemicolon: false
+        afterForSemicolon: true
+        afterTypeCast: true
+        beforeColonInForEach: true
+        insideOneLineEnumBraces: false
+        aroundEqualInAnnotationValuePair: true
+      typeArguments:
+        afterComma: true
+        beforeOpeningAngleBracket: false
+        afterClosingAngleBracket: false
diff --git a/storm-checkstyle/README.md b/storm-checkstyle/README.md
new file mode 100644
index 000000000..630718545
--- /dev/null
+++ b/storm-checkstyle/README.md
@@ -0,0 +1,144 @@
+# Checkstyle auto-fix
+
+`storm_checkstyle.xml` is the Checkstyle ruleset enforced on every `mvn` 
build. 
+Many rules are `severity=warning` as they do not fail the build, but they 
should still be 
+slimmed to the minimal amount
+
+The auto-fix is **not** part of the normal build. It is run on demand in two 
stages, 
+in every module that declares the plugins:
+
+1. **`rewrite-maven-plugin` (OpenRewrite)** - reduced amount of fixes done on 
a parsed Java AST. 
+   The recipe list is `rewrite.yml` at the repo root 
(`org.apache.storm.checkstyle.AutoFix`).
+2. **`src/main/resources/storm/checkstyle-autofix.groovy`** (via 
`gmavenplus-plugin`) -
+   runs Checkstyle itself, in-process, with the same version and ruleset the 
build
+   uses, reads violations (file, line, column, message) and hands each one to 
a small fixer written for the one
+   Checkstyle module that reported it. The cycle until no more rules can be 
fixed.
+
+The values a fixer needs (`Indentation` `basicOffset`, `LineLength` `max`,
+`CustomImportOrder` rules) are read from the ruleset XML at run time avoiding 
hardcoding. Files that the
+module excludes from Checkstyle (`<excludes>` of `maven-checkstyle-plugin`, 
e.g. thrift
+generated code) are neither audited nor rewritten.
+
+The license comment at the top of a file is never touched, whatever Checkstyle 
reports in it
+(`LineLength` on a wide-wrapped header, `JavadocParagraph` on one opened with 
`/**`): both
+wrappings of the ASF header are accepted, see [License 
headers](#license-headers).
+
+Safety net, per file: a file that stops parsing after a fix is restored to the 
state
+before that round; a file that ends with more violations than it started with 
is
+restored to its original text.
+
+## What it fixes
+
+| Checkstyle module | Stage |
+|---|---|
+| `NeedBraces`, `MultipleVariableDeclarations`, `ModifierOrder`, `UpperEll`, 
`ArrayTypeStyle`, `FileTabCharacter`, `OperatorWrap` | OpenRewrite (dedicated 
recipes) |
+| `WhitespaceAround`, `WhitespaceAfter`, `NoWhitespaceBefore`, `ParenPad`, 
`MethodParamPad`, `GenericWhitespace` | OpenRewrite (`Spaces`, style 
`org.apache.storm.checkstyle.Style`) |
+| `Indentation`, `EmptyLineSeparator`, `CustomImportOrder` | OpenRewrite 
(`TabsAndIndents`, `BlankLines`, `OrderImports`, same style) |
+| `AvoidStarImport` | OpenRewrite (`RemoveUnusedImports`; needs type 
attribution, i.e. the module's dependencies resolvable) |
+| `MissingSwitchDefault` | OpenRewrite (`DefaultComesLast`: only moves an 
existing `default` last, it does not add a missing one) |
+| `LeftCurly`, `RightCurly`, `NoLineWrap`, `AnnotationLocation` | OpenRewrite 
(`WrappingAndBraces`), partial: it does not move `{`, `catch`/`finally` or 
every annotation; what is left is not chased in Groovy |
+| `NoWhitespaceBeforeCaseDefaultColon`, `RegexpSinglelineJava` (empty-block 
spacing) | OpenRewrite (`Spaces`), partial: it writes `{ }` for empty blocks, 
which `RegexpSinglelineJava` rejects |
+| `CommentsIndentation` | OpenRewrite (`TabsAndIndents`), partial |
+| `OneStatementPerLine`, `SeparatorWrap` | Groovy (no recipe) |
+| `LineLength` | Groovy: re-wraps comments and Javadoc, and breaks code at 
commas, `&&`/`\|\|`/`+`/`?`, `.method(` chains and `=`, and splits long string 
literals into `"a" + "b"` |
+| `IllegalTokenText`, `AvoidEscapedUnicodeCharacters`, `TodoComment`, 
single-line comment space | Groovy |
+| `OverloadMethodsDeclarationOrder`, `ConstructorsDeclarationGrouping` | 
Groovy: the member is moved next to its overloads/constructors |
+| `JavadocLeadingAsteriskAlign`, `JavadocMissingLeadingAsterisk`, 
`JavadocContentLocation`, `JavadocParagraph`, 
`JavadocTagContinuationIndentation`, `RequireEmptyLineBeforeBlockTagGroup`, 
`AtclauseOrder`, `InvalidJavadocPosition`, `SummaryJavadoc` (missing period, 
lowercase first word) | Groovy |
+
+The OpenRewrite style mirrors the ruleset by hand (`rewrite.yml`); keep 
`continuationIndent`
+at 8, the convention of the code base (Checkstyle's `lineWrappingIndentation` 
of 4 is a minimum).
+
+## License headers
+
+`storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy` 
rewrites the
+leading license comment of the module's Java, Groovy, XML, Markdown, YAML, 
properties, Python and
+shell files to the exact template of
+https://www.apache.org/legal/src-headers.html#headers, but only when its 
wording is wrong (a typo,
+missing words). A header with the words of the template is left alone whatever 
its line wrapping,
+its case (the older `to You` form) or the Javadoc `<p>` an IDE formatter 
added: rewrapping it would
+touch most files of the code base and pollute `git blame` for no legal gain. 
Only the license lines
+are replaced: `#` comment lines around them (a `-*- coding -*-` line, a note 
on the script's
+arguments) are kept, and a `/* */` or `<!-- -->` comment with anything besides 
the license is left
+alone. A file whose leading comment is not an ASF license header, or that 
carries a copyright
+notice of anybody but the ASF (a third-party work), is left alone too:
+
+```sh
+mvn -pl <module> 
org.codehaus.gmavenplus:gmavenplus-plugin:execute@normalize-license-headers
+```
+
+Add `-Dlicense.check=true` to only report (the build fails if a file would 
change).
+
+## Not fixed
+
+The following violations can't be fixed mechanically in a satisfactory way:
+
+- `MissingJavadocMethod`, `MissingJavadocType`, `JavadocMethod`, 
`NonEmptyAtclauseDescription`,
+  `SingleLineJavadoc`, the rest of `SummaryJavadoc` - human intervention for 
docs.
+- `MethodName`, the other naming rules and `AbbreviationAsWordInName` - a 
rename needs
+  whole-repo symbol resolution.
+- `FallThrough`, `EmptyCatchBlock` - comments could silence bugs.
+- `VariableDeclarationUsageDistance` - moving a declaration can reorder side 
effects.
+- `OneTopLevelClass`, `OuterTypeFilename`, `LeftCurly` on `case X: {`,
+  `TextBlockGoogleStyleFormatting`.
+- `LineLength` on lines with no safe break point (long unbreakable tokens, 
URLs).
+
+### Partial OpenRewrite coverage
+
+`WrappingAndBraces` (custom `WrappingAndBracesStyle`) does not move `{` to the 
end of the line and
+leaves `catch`/`finally` and some annotations where they are; human 
intervention is needed.
+
+### Rejected OpenRewrite recipes
+
+`org.openrewrite.java.format.AutoFormat` (bundles formatters that ignore the 
ruleset and
+introduces star imports) and `TypecastParenPad` (turns `(T) x` into `(T)x`, 
violating
+`WhitespaceAfter`) are not used.
+
+## Running it
+
+Three passes: OpenRewrite, the Groovy stage, then the OpenRewrite re-indent 
pass (the Groovy
+`LineLength` wraps leave continuation lines mis-indented; the pass runs 
`TabsAndIndents` only, since
+`Spaces` would turn the `{}` of empty blocks back into `{ }`). Review the 
result with `git diff`:
+
+```sh
+mvn 
org.openrewrite.maven:rewrite-maven-plugin:run@checkstyle-autofix-openrewrite \
+    org.codehaus.gmavenplus:gmavenplus-plugin:execute@checkstyle-autofix \
+    
org.openrewrite.maven:rewrite-maven-plugin:run@checkstyle-reindent-openrewrite \
+    -pl '!storm-shaded-deps' -Dcheckstyle.skip=true
+```
+
+`-Dcheckstyle.skip=true` is needed: `rewrite:run` forks the lifecycle up to
+`process-test-classes`, which would otherwise run the `validate`-bound 
checkstyle check
+and block the auto-fix on the very violations it is meant to fix. The Groovy 
stage runs
+Checkstyle itself and is unaffected by the flag.
+
+For a single module, replace the `-pl` argument (e.g. `-pl storm-client`); the 
Groovy
+stage may need a second invocation of the same command if you want to be sure 
nothing
+is left, since each run already repeats until nothing more can be fixed. To 
confirm
+the result against the build's own rules afterwards:
+
+```sh
+mvn validate -pl '!storm-shaded-deps'
+```
+
+### From GitHub Actions
+
+The `Checkstyle auto-fix` workflow 
(`.github/workflows/checkstyle-autofix.yaml`, run by hand from
+the Actions tab) runs the three passes and the license header normalization on 
one module. It
+pushes the result to a new branch `checkstyle-fix/<module>-<run id>-<attempt>` 
and opens a pull
+request from it against the branch the workflow ran on. The build's Checkstyle 
check runs before
+and after the fix: the counts, and the violations left per check, are in the 
pull request and the
+run summary, and both full reports are in the run's `checkstyle-reports` 
artifact. The module must
+declare `rewrite-maven-plugin` and `gmavenplus-plugin` (directly or through 
its parent), otherwise
+the job fails instead of silently fixing nothing.
+
+### Reviewing what it did
+
+Both stages edit source files in place, so `git diff` shows exactly what 
changed.
+
+### Upgrading Checkstyle
+
+`checkstyle.version` (root `pom.xml`) is used by both the Checkstyle check and 
the
+Groovy stage, so they can't disagree. If you change it, also update
+`storm_checkstyle.xml` to match the new `google_checks.xml`: the Groovy fixers 
parse
+Checkstyle's message text, and a reworded message makes that fixer skip the 
violation
+(nothing breaks - Checkstyle still reports it).
diff --git 
a/storm-checkstyle/src/main/resources/storm/checkstyle-autofix.groovy 
b/storm-checkstyle/src/main/resources/storm/checkstyle-autofix.groovy
new file mode 100644
index 000000000..295a01155
--- /dev/null
+++ b/storm-checkstyle/src/main/resources/storm/checkstyle-autofix.groovy
@@ -0,0 +1,1633 @@
+/*
+ * 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
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Auto-fixes storm_checkstyle.xml violations.
+ *
+ * Script runs Checkstyle itself (in-process, same version and same ruleset 
the build's checkstyle check uses) and
+ * reads back the exact violations - file, line, column, message.
+ * Each violation is handed to a small fixer written for the one
+ * Checkstyle module that reported it, which edits only the text that 
violation points at.
+ * Because the fixers act on Checkstyle's own reports, they never touch code 
Checkstyle is
+ * happy with, and there is no formatter with a style of its own to drift from 
the
+ * ruleset. The audit/fix cycle repeats (a fix can unmask or move another 
violation)
+ * until nothing more can be fixed. Values a fixer needs (Indentation's 
basicOffset,
+ * LineLength's max, CustomImportOrder's rule string) are read straight from 
the ruleset
+ * XML; a fixer whose parameters aren't declared there is skipped rather than 
guessing.
+ *
+ * The license comment at the top of a file is never touched, whatever 
Checkstyle reports in it
+ * (LineLength on a wide-wrapped header, JavadocParagraph on one opened with 
'/**'): both
+ * wrappings of the ASF header are accepted (see 
normalize-license-headers.groovy), and
+ * re-laying them out would touch most files of the code base for no gain.
+ *
+ * Safety net, per file: a file that stops parsing after a fix is restored to 
its state
+ * before the last round and left alone; a file that ends up with more 
violations than it
+ * started with is restored to its original text.
+ *
+ * Only the rules OpenRewrite has no recipe for are fixed here (everything 
OpenRewrite can do,
+ * fully or partly, runs in its stage, see rewrite.yml: the leftovers are not 
chased here).
+ * Fixers (by Checkstyle module):
+ *   Layout    OneStatementPerLine, LineLength (comments, 
argument/operator/call-chain wraps,
+ *             long string literals)
+ *   Wrapping  SeparatorWrap
+ *   Ordering  OverloadMethodsDeclarationOrder, ConstructorsDeclarationGrouping
+ *   Style     IllegalTokenText, AvoidEscapedUnicodeCharacters, TodoComment,
+ *             single-line-comment-space (MatchXpath)
+ *   Javadoc   JavadocLeadingAsteriskAlign, JavadocMissingLeadingAsterisk,
+ *             JavadocContentLocation, JavadocParagraph, 
JavadocTagContinuationIndentation,
+ *             RequireEmptyLineBeforeBlockTagGroup, AtclauseOrder, 
SummaryJavadoc
+ *             (missing period, lowercase first word), InvalidJavadocPosition
+ *
+ * Deliberately not fixed - each needs a decision or knowledge a text edit 
can't have:
+ *   Naming rules and AbbreviationAsWordInName (renames need cross-file symbol 
resolution),
+ *   the MissingJavadoc checks, JavadocMethod and
+ *   NonEmptyAtclauseDescription (need real prose), FallThrough and 
EmptyCatchBlock (a
+ *   comment would only silence a possible bug), 
VariableDeclarationUsageDistance (moving a
+ *   declaration can reorder side effects), 
OneTopLevelClass/OuterTypeFilename, LeftCurly on
+ *   `case X: {`, TextBlockGoogleStyleFormatting. Checkstyle keeps checking 
all of these.
+ *
+ * Run on demand via gmavenplus-plugin's `execute` goal (execution id 
`checkstyle-autofix`,
+ * bound to no phase, so a normal build never rewrites sources); see 
storm-checkstyle/README.md
+ * for the command. Declared per-module (root pom pluginManagement) since
+ * project.compileSourceRoots / testCompileSourceRoots are module-specific.
+ */
+
+import com.puppycrawl.tools.checkstyle.Checker
+import com.puppycrawl.tools.checkstyle.ConfigurationLoader
+import com.puppycrawl.tools.checkstyle.PropertiesExpander
+import com.puppycrawl.tools.checkstyle.api.AuditEvent
+import com.puppycrawl.tools.checkstyle.api.AuditListener
+import groovy.io.FileType
+import groovy.transform.CompileStatic
+import groovy.xml.XmlSlurper
+
+import java.util.regex.Matcher
+
+// 
=================================================================================
+// Src: a mutable source text plus the scanning helpers the fixers share. 
Everything is
+// expressed in character offsets into the *current* text. `mask` classifies 
each char as
+// code / comment / string-or-char-literal, so scans never mistake the 
contents of a
+// string or comment for syntax.
+// 
=================================================================================
+
+@CompileStatic
+class Src {
+    
+static final byte CODE = 0
+    
+static final byte COMMENT = 1
+    
+static final byte LITERAL = 2
+    
+
+private final StringBuilder sb
+    
+private byte[] maskCache
+
+    Src(String text) {
+        this.sb = new StringBuilder(text)
+    }
+
+    @Override
+    String toString() {
+        sb.toString()
+    }
+
+    int length() {
+        sb.length()
+    }
+
+    char charAt(int i) {
+        sb.charAt(i)
+    }
+
+    String slice(int from, int to) {
+        sb.substring(from, to)
+    }
+
+    int indexOf(String s, int from) {
+        sb.indexOf(s, from)
+    }
+
+    int lastIndexOf(String s, int from) {
+        sb.lastIndexOf(s, from)
+    }
+
+    boolean has(int i, String s) {
+        if (i < 0 || i + s.length() > sb.length()) {
+            return false
+        }
+        for (int k = 0; k < s.length(); k++) {
+            if (sb.charAt(i + k) != s.charAt(k)) {
+                return false
+            }
+        }
+        true
+    }
+
+    void replace(int from, int to, String s) {
+        sb.replace(from, to, s)
+        maskCache = null
+    }
+
+    void insert(int at, String s) {
+        sb.insert(at, s)
+        maskCache = null
+    }
+
+    void delete(int from, int to) {
+        sb.delete(from, to)
+        maskCache = null
+    }
+
+    // ---- lines 
------------------------------------------------------------------
+
+    int lineStart(int off) {
+        off <= 0 ? 0 : sb.lastIndexOf('\n', off - 1) + 1
+    }
+
+    int lineEnd(int off) {
+        int i = sb.indexOf('\n', off)
+        i < 0 ? sb.length() : i
+    }
+
+    String line(int off) {
+        slice(lineStart(off), lineEnd(off))
+    }
+
+    boolean isHorizontalWs(int i) {
+        char c = sb.charAt(i)
+        c == (char) ' ' || c == (char) '\t'
+    }
+
+    /** Index of the first non-blank char on the line that starts at ls (or 
the line end). */
+    int firstNonWs(int ls) {
+        int i = ls
+        while (i < sb.length() && isHorizontalWs(i)) {
+            i++
+        }
+        i
+    }
+
+    String indentOf(int off) {
+        int ls = lineStart(off)
+        slice(ls, firstNonWs(ls))
+    }
+
+    boolean isBlankLine(int off) {
+        line(off).trim().isEmpty()
+    }
+
+    /** True if the line holds only comment text (and whitespace). */
+    boolean isCommentOnlyLine(int off) {
+        int ls = lineStart(off)
+        int le = lineEnd(off)
+        boolean seen = false
+        for (int i = ls; i < le; i++) {
+            if (Character.isWhitespace(sb.charAt(i))) {
+                continue
+            }
+            if (mask()[i] != COMMENT) {
+                return false
+            }
+            seen = true
+        }
+        seen
+    }
+
+    // ---- code/comment/literal classification 
------------------------------------
+
+    byte[] mask() {
+        if (maskCache == null) {
+            maskCache = computeMask(sb)
+        }
+        maskCache
+    }
+
+    boolean isCode(int i) {
+        i >= 0 && i < sb.length() && mask()[i] == CODE
+    }
+
+    boolean isComment(int i) {
+        i >= 0 && i < sb.length() && mask()[i] == COMMENT
+    }
+
+    boolean isLiteral(int i) {
+        i >= 0 && i < sb.length() && mask()[i] == LITERAL
+    }
+
+    boolean isCodeChar(int i) {
+        isCode(i) && !Character.isWhitespace(sb.charAt(i))
+    }
+
+    /** Last code (non-whitespace, non-comment, non-literal-interior is NOT 
excluded) char at or before i. */
+    int prevCode(int i) {
+        byte[] m = mask()
+        for (int j = Math.min(i, sb.length() - 1); j >= 0; j--) {
+            if (m[j] != COMMENT && !Character.isWhitespace(sb.charAt(j))) {
+                return j
+            }
+        }
+        -1
+    }
+
+    /** First code char at or after i (literals count: their opening quote is 
returned). */
+    int nextCode(int i) {
+        byte[] m = mask()
+        for (int j = Math.max(i, 0); j < sb.length(); j++) {
+            if (m[j] != COMMENT && !Character.isWhitespace(sb.charAt(j))) {
+                return j
+            }
+        }
+        -1
+    }
+
+    /** Index of the bracket closing the one at open, ignoring comments and 
literals; -1 if none. */
+    int matchClose(int open) {
+        char o = sb.charAt(open)
+        char c = o == (char) '(' ? (char) ')' : (o == (char) '[' ? (char) ']' 
: (char) '}')
+        byte[] m = mask()
+        int depth = 0
+        for (int i = open; i < sb.length(); i++) {
+            if (m[i] != CODE) {
+                continue
+            }
+            char ch = sb.charAt(i)
+            if (ch == o) {
+                depth++
+            } else if (ch == c) {
+                depth--
+                if (depth == 0) {
+                    return i
+                }
+            }
+        }
+        -1
+    }
+
+    /** Index of the bracket opening the one at close; -1 if none. */
+    int matchOpen(int close) {
+        char c = sb.charAt(close)
+        char o = c == (char) ')' ? (char) '(' : (c == (char) ']' ? (char) '[' 
: (char) '{')
+        byte[] m = mask()
+        int depth = 0
+        for (int i = close; i >= 0; i--) {
+            if (m[i] != CODE) {
+                continue
+            }
+            char ch = sb.charAt(i)
+            if (ch == c) {
+                depth++
+            } else if (ch == o) {
+                depth--
+                if (depth == 0) {
+                    return i
+                }
+            }
+        }
+        -1
+    }
+
+    /** The Java identifier/keyword starting at i ('' if none). */
+    String wordAt(int i) {
+        int j = i
+        while (j < sb.length() && 
Character.isJavaIdentifierPart(sb.charAt(j))) {
+            j++
+        }
+        slice(i, j)
+    }
+
+    static byte[] computeMask(CharSequence t) {
+        int n = t.length()
+        byte[] m = new byte[n]
+        int i = 0
+        while (i < n) {
+            char c = t.charAt(i)
+            char d = i + 1 < n ? t.charAt(i + 1) : (char) 0
+            if (c == (char) '/' && d == (char) '/') {
+                int j = i
+                while (j < n && t.charAt(j) != (char) '\n') {
+                    m[j++] = COMMENT
+                }
+                i = j
+            } else if (c == (char) '/' && d == (char) '*') {
+                int j = i + 2
+                while (j < n && !(t.charAt(j) == (char) '*' && j + 1 < n && 
t.charAt(j + 1) == (char) '/')) {
+                    j++
+                }
+                int end = Math.min(n, j + 2)
+                for (int k = i; k < end; k++) {
+                    m[k] = COMMENT
+                }
+                i = end
+            } else if (c == (char) '"' && d == (char) '"' && i + 2 < n && 
t.charAt(i + 2) == (char) '"') {
+                int j = i + 3
+                while (j < n) {
+                    char x = t.charAt(j)
+                    if (x == (char) '\\') {
+                        j += 2
+                    } else if (x == (char) '"' && j + 2 < n + 0 && t.charAt(j 
+ 1) == (char) '"'
+                            && t.charAt(j + 2) == (char) '"') {
+                        j += 3
+                        break
+                    } else {
+                        j++
+                    }
+                }
+                int end = Math.min(n, j)
+                for (int k = i; k < end; k++) {
+                    m[k] = LITERAL
+                }
+                i = end
+            } else if (c == (char) '"' || c == (char) '\'') {
+                int j = i + 1
+                while (j < n && t.charAt(j) != c && t.charAt(j) != (char) 
'\n') {
+                    j += t.charAt(j) == (char) '\\' ? 2 : 1
+                }
+                int end = Math.min(n, j < n && t.charAt(j) == c ? j + 1 : j)
+                for (int k = i; k < end; k++) {
+                    m[k] = LITERAL
+                }
+                i = end
+            } else {
+                i++
+            }
+        }
+        m
+    }
+}
+
+// 
=================================================================================
+// Fixers. Every fixer has the signature
+//     static int fix(Src s, int off, String msg, int[] origLineStarts)
+// `off` is the offset Checkstyle reported (already translated to the current 
text; it is
+// only ever handed to a fixer while everything before the previous edit is 
unchanged),
+// `msg` its message. A fixer returns the lowest offset it modified, or NONE 
if it changed
+// nothing. Violations are applied last-to-first, and any violation at or 
beyond an
+// already-modified offset is deferred to the next audit round.
+// 
=================================================================================
+
+class Fixers {
+    
+static final int NONE = -1
+    
+
+/** Set from the ruleset: Indentation.basicOffset, LineLength.max. */
+static int basicOffset = 4
+    
+static int maxLineLength = 100
+
+    // ---- small helpers 
----------------------------------------------------------
+
+    static String tokenOf(String msg) {
+        Matcher m = msg =~ /'([^']+)'/
+        m.find() ? m.group(1) : null
+    }
+
+    static boolean hws(char c) {
+        c == ' ' as char || c == '\t' as char
+    }
+
+    static String spaces(int n) {
+        ' ' * Math.max(0, n)
+    }
+
+    static String nl(String indent) {
+        '\n' + indent
+    }
+
+    static String oneIndent() {
+        spaces(basicOffset)
+    }
+
+    /** Start of the run of comment lines that sit directly on top of the 
declaration at off. */
+    static int leadingBlockStart(Src s, int off) {
+        int ls = s.lineStart(off)
+        while (ls > 0) {
+            int pls = s.lineStart(ls - 1)
+            if (s.isBlankLine(pls) || !s.isCommentOnlyLine(pls)) {
+                break
+            }
+            ls = pls
+        }
+        ls
+    }
+
+    /** End (exclusive) of the member whose header starts at off: after its 
body's '}' or its ';'. */
+    static int memberEnd(Src s, int off) {
+        int n = s.length()
+        int i = off
+        while (i < n) {
+            if (!s.isCode(i)) {
+                i++
+                continue
+            }
+            char c = s.charAt(i)
+            if (c == '(' as char || c == '[' as char) {
+                int e = s.matchClose(i)
+                if (e < 0) {
+                    return -1
+                }
+                i = e + 1
+                continue
+            }
+            if (c == ';' as char) {
+                return i + 1
+            }
+            if (c == '{' as char) {
+                int e = s.matchClose(i)
+                return e < 0 ? -1 : e + 1
+            }
+            i++
+        }
+        -1
+    }
+
+    /** End (exclusive) of the statement whose first token is at i; -1 if it 
can't be determined. */
+    static int stmtEnd(Src s, int from) {
+        int i = s.nextCode(from)
+        if (i < 0) {
+            return -1
+        }
+        char c = s.charAt(i)
+        if (c == '{' as char) {
+            int e = s.matchClose(i)
+            return e < 0 ? -1 : e + 1
+        }
+        if (c == ';' as char) {
+            return i + 1
+        }
+        String w = s.wordAt(i)
+        switch (w) {
+            case 'if':
+            case 'for':
+            case 'while':
+                int p = s.nextCode(i + w.length())
+                if (p < 0 || s.charAt(p) != '(' as char) {
+                    return -1
+                }
+                int close = s.matchClose(p)
+                if (close < 0) {
+                    return -1
+                }
+                int body = stmtEnd(s, close + 1)
+                if (w == 'if' && body >= 0) {
+                    int nxt = s.nextCode(body)
+                    if (nxt >= 0 && s.wordAt(nxt) == 'else') {
+                        return stmtEnd(s, nxt + 4)
+                    }
+                }
+                return body
+            case 'do':
+                int b = stmtEnd(s, i + 2)
+                if (b < 0) {
+                    return -1
+                }
+                int wh = s.nextCode(b)
+                if (wh < 0 || s.wordAt(wh) != 'while') {
+                    return -1
+                }
+                int wp = s.nextCode(wh + 5)
+                int wc = wp < 0 ? -1 : s.matchClose(wp)
+                int semi = wc < 0 ? -1 : s.nextCode(wc + 1)
+                return semi >= 0 && s.charAt(semi) == ';' as char ? semi + 1 : 
-1
+            case 'try':
+                int tp = s.nextCode(i + 3)
+                if (tp >= 0 && s.charAt(tp) == '(' as char) {
+                    int rc = s.matchClose(tp)
+                    tp = rc < 0 ? -1 : s.nextCode(rc + 1)
+                }
+                if (tp < 0 || s.charAt(tp) != '{' as char) {
+                    return -1
+                }
+                int e = s.matchClose(tp) + 1
+                while (e > 0) {
+                    int nxt = s.nextCode(e)
+                    String nw = nxt < 0 ? '' : s.wordAt(nxt)
+                    if (nw == 'catch') {
+                        int cp = s.nextCode(nxt + 5)
+                        int cc = cp < 0 ? -1 : s.matchClose(cp)
+                        int cb = cc < 0 ? -1 : s.nextCode(cc + 1)
+                        if (cb < 0 || s.charAt(cb) != '{' as char) {
+                            return -1
+                        }
+                        e = s.matchClose(cb) + 1
+                    } else if (nw == 'finally') {
+                        int fb = s.nextCode(nxt + 7)
+                        if (fb < 0 || s.charAt(fb) != '{' as char) {
+                            return -1
+                        }
+                        e = s.matchClose(fb) + 1
+                    } else {
+                        break
+                    }
+                }
+                return e > 0 ? e : -1
+            case 'switch':
+            case 'synchronized':
+                int sp = s.nextCode(i + w.length())
+                int sc = sp < 0 || s.charAt(sp) != '(' as char ? -1 : 
s.matchClose(sp)
+                int sb2 = sc < 0 ? -1 : s.nextCode(sc + 1)
+                if (sb2 < 0 || s.charAt(sb2) != '{' as char) {
+                    return -1
+                }
+                int se = s.matchClose(sb2)
+                return se < 0 ? -1 : se + 1
+            default:
+                break
+        }
+        int j = i
+        int n = s.length()
+        while (j < n) {
+            if (!s.isCode(j)) {
+                j++
+                continue
+            }
+            char ch = s.charAt(j)
+            if (ch == '(' as char || ch == '[' as char || ch == '{' as char) {
+                int e = s.matchClose(j)
+                if (e < 0) {
+                    return -1
+                }
+                j = e + 1
+                continue
+            }
+            if (ch == ';' as char) {
+                return j + 1
+            }
+            j++
+        }
+        -1
+    }
+
+    // ---- spacing 
----------------------------------------------------------------
+
+    static int commentSpace(Src s, int off, String msg, int[] ls) {
+        if (msg.contains('must be followed by a whitespace') && s.has(off, 
'//')) {
+            s.insert(off + 2, ' ')
+            return off + 2
+        }
+        NONE
+    }
+
+    // ---- line structure 
---------------------------------------------------------
+
+    /** OneStatementPerLine reports the ';' of every statement after the first 
on a line. */
+    static int oneStatementPerLine(Src s, int off, String msg, int[] ls) {
+        if (!s.has(off, ';')) {
+            return NONE
+        }
+        int lineStart = s.lineStart(off)
+        int j = off - 1
+        while (j >= lineStart) {
+            if (!s.isCode(j)) {
+                j--
+                continue
+            }
+            char c = s.charAt(j)
+            if (c == ')' as char || c == ']' as char) {
+                int open = s.matchOpen(j)
+                if (open < lineStart) {
+                    return NONE
+                }
+                j = open - 1
+                continue
+            }
+            if (c == ';' as char || c == '{' as char || c == '}' as char) {
+                break
+            }
+            j--
+        }
+        if (j < lineStart) {
+            return NONE
+        }
+        int k = j + 1
+        while (k < off && s.isHorizontalWs(k)) {
+            k++
+        }
+        if (k >= off || !s.isCode(k)) {
+            return NONE
+        }
+        s.replace(j + 1, k, nl(s.indentOf(off)))
+        j + 1
+    }
+
+    // ---- wrapping 
---------------------------------------------------------------
+
+    /** Operator/'.'/'::' left dangling at a line's end: move it to the start 
of the next line. */
+    static int moveToNextLine(Src s, int off, String tok, boolean spaceAfter) {
+        if (!s.has(off, tok)) {
+            return NONE
+        }
+        int lineStart = s.lineStart(off)
+        int le = s.lineEnd(off)
+        String rest = s.slice(off + tok.length(), le).trim()
+        String comment = null
+        if (!rest.isEmpty()) {
+            if (!rest.startsWith('//')) {
+                return NONE
+            }
+            comment = rest
+        }
+        if (le >= s.length() - 1) {
+            return NONE
+        }
+        int nls = le + 1
+        int nfn = s.firstNonWs(nls)
+        if (nfn >= s.lineEnd(nls) || s.isComment(nfn)) {
+            return NONE
+        }
+        int j = off
+        while (j > lineStart && s.isHorizontalWs(j - 1)) {
+            j--
+        }
+        if (j == lineStart) {
+            return NONE
+        }
+        s.insert(nfn, tok + (spaceAfter ? ' ' : ''))
+        s.replace(j, le, comment != null ? ' ' + comment : '')
+        j
+    }
+
+    /** ',' or '...' that starts a line: move it to the end of the previous 
line's code. */
+    static int moveToPrevLine(Src s, int off, String tok) {
+        if (!s.has(off, tok)) {
+            return NONE
+        }
+        int lineStart = s.lineStart(off)
+        if (lineStart == 0 || !s.slice(lineStart, off).trim().isEmpty()) {
+            return NONE
+        }
+        int pc = s.prevCode(lineStart - 1)
+        if (pc < 0) {
+            return NONE
+        }
+        int after = off + tok.length()
+        int k = after
+        while (k < s.length() && s.isHorizontalWs(k)) {
+            k++
+        }
+        int le = s.lineEnd(off)
+        if (k >= le) {
+            s.delete(lineStart, Math.min(s.length(), le + 1))
+        } else {
+            s.delete(off, k)
+        }
+        s.insert(pc + 1, tok)
+        pc + 1
+    }
+
+    static int separatorWrap(Src s, int off, String msg, int[] ls) {
+        String tok = tokenOf(msg)
+        if (tok == null) {
+            return NONE
+        }
+        if (msg.contains('should be on a new line')) {
+            String line = s.line(off).trim()
+            if (line.startsWith('import ') || line.startsWith('package ')) {
+                return NONE
+            }
+            return moveToNextLine(s, off, tok, false)
+        }
+        if (msg.contains('should be on the previous line')) {
+            return moveToPrevLine(s, off, tok)
+        }
+        NONE
+    }
+
+    // ---- ordering 
---------------------------------------------------------------
+
+    /** Moves the member at off (with its leading comments) to just after the 
member at targetLine. */
+    static int moveMemberAfter(Src s, int off, int targetLine, int[] 
origLineStarts) {
+        if (targetLine < 1 || targetLine > origLineStarts.length) {
+            return NONE
+        }
+        int cs = leadingBlockStart(s, off)
+        int ce = memberEnd(s, off)
+        int t = s.firstNonWs(origLineStarts[targetLine - 1])
+        int te = t >= cs ? -1 : memberEnd(s, t)
+        if (ce < 0 || te < 0 || te > cs) {
+            return NONE
+        }
+        String member = s.slice(cs, ce)
+        int del = ce
+        if (del < s.length() && s.charAt(del) == '\n' as char) {
+            del++
+        }
+        s.delete(cs, del)
+        if (cs > 0 && s.isBlankLine(s.lineStart(cs - 1)) && (cs >= s.length() 
|| s.isBlankLine(cs)
+                || s.line(cs).trim().startsWith('}'))) {
+            int pb = s.lineStart(cs - 1)
+            s.delete(pb, Math.min(s.length(), s.lineEnd(pb) + 1))
+        }
+        s.insert(te, '\n\n' + member)
+        te
+    }
+
+    static int overloadOrder(Src s, int off, String msg, int[] ls) {
+        Matcher m = msg =~ /line '(\d+)'/
+        m.find() ? moveMemberAfter(s, off, m.group(1) as int, ls) : NONE
+    }
+
+    static int constructorGrouping(Src s, int off, String msg, int[] ls) {
+        Matcher m = msg =~ /line '(\d+)'/
+        m.find() ? moveMemberAfter(s, off, m.group(1) as int, ls) : NONE
+    }
+    
+
+// ---- literals 
---------------------------------------------------------------
+
+private static final Map<Integer, String> ESCAPES = [
+        8: '\\b', 9: '\\t', 10: '\\n', 12: '\\f', 13: '\\r', 32: ' ', 34: 
'\\"', 39: "\\'", 92: '\\\\']
+
+    static int literalEnd(Src s, int off) {
+        int e = off
+        while (e < s.length() && s.isLiteral(e)) {
+            e++
+        }
+        e
+    }
+
+    static int illegalTokenText(Src s, int off, String msg, int[] ls) {
+        if (!s.isLiteral(off)) {
+            return NONE
+        }
+        int start = off
+        while (start > 0 && s.isLiteral(start - 1)) {
+            start--
+        }
+        // text blocks are left alone: there '\u0020' / '\040' keep a trailing 
space that a raw space
+        // would lose to the text block's whitespace stripping
+        if (s.has(start, '"""')) {
+            return NONE
+        }
+        int end = literalEnd(s, off)
+        String lit = s.slice(off, end)
+        StringBuilder out = new StringBuilder()
+        int i = 0
+        while (i < lit.length()) {
+            char c = lit.charAt(i)
+            if (c != '\\' as char || i + 1 >= lit.length()) {
+                out.append(c)
+                i++
+                continue
+            }
+            Matcher u = lit.substring(i) =~ /^\\u+([0-9a-fA-F]{4})/
+            Matcher o = lit.substring(i) =~ 
/^\\(0(?:10|11|12|14|15|40|42|47)|134)(?![0-7])/
+            if (u.find() && ESCAPES.containsKey(Integer.parseInt(u.group(1), 
16))) {
+                out.append(ESCAPES[Integer.parseInt(u.group(1), 16)])
+                i += u.end()
+            } else if (o.find()) {
+                out.append(ESCAPES[Integer.parseInt(o.group(1), 8)])
+                i += o.end()
+            } else {
+                out.append(c).append(lit.charAt(i + 1))
+                i += 2
+            }
+        }
+        if (out.toString() == lit) {
+            return NONE
+        }
+        s.replace(off, end, out.toString())
+        off
+    }
+
+    static int avoidEscapedUnicode(Src s, int off, String msg, int[] ls) {
+        int start = off
+        // the reported column may be the string's opening quote or the escape 
itself
+        if (!s.isLiteral(off)) {
+            return NONE
+        }
+        while (start > 0 && s.isLiteral(start - 1)) {
+            start--
+        }
+        int end = literalEnd(s, off)
+        String lit = s.slice(start, end)
+        Matcher m = lit =~ 
/(?<!\\)((?:\\\\)*)\\u+([0-9a-fA-F]{4})(?:\\u+([0-9a-fA-F]{4}))?/
+        StringBuffer out = new StringBuffer()
+        boolean changed = false
+        while (m.find()) {
+            String pair = m.group(3) == null ? '' : m.group(3)
+            String chars = new 
String(Character.toChars(Integer.parseInt(m.group(2), 16)))
+            if (pair) {
+                chars += new String(Character.toChars(Integer.parseInt(pair, 
16)))
+                if (chars.codePointCount(0, chars.length()) != 1) {
+                    chars = new 
String(Character.toChars(Integer.parseInt(m.group(2), 16)))
+                    m.appendReplacement(out, 
Matcher.quoteReplacement(m.group()))
+                    continue
+                }
+            }
+            int cp = chars.codePointAt(0)
+            int type = Character.getType(cp)
+            boolean printable = !(Character.isISOControl(cp) || 
Character.isWhitespace(cp) || Character.isSpaceChar(cp)
+                    || type == Character.FORMAT || type == 
Character.UNASSIGNED || type == Character.PRIVATE_USE
+                    || type == Character.SURROGATE || type == 
Character.LINE_SEPARATOR
+                    || type == Character.PARAGRAPH_SEPARATOR || cp == 34 || cp 
== 39 || cp == 92)
+            if (!printable || Character.charCount(cp) != chars.length()) {
+                m.appendReplacement(out, Matcher.quoteReplacement(m.group()))
+                continue
+            }
+            m.appendReplacement(out, Matcher.quoteReplacement(m.group(1) + 
chars))
+            changed = true
+        }
+        m.appendTail(out)
+        if (!changed) {
+            return NONE
+        }
+        s.replace(start, end, out.toString())
+        start
+    }
+
+    static int todoComment(Src s, int off, String msg, int[] ls) {
+        int i = off
+        while (i < s.length() && s.isHorizontalWs(i)) {
+            i++
+        }
+        if (i + 4 > s.length() || !s.slice(i, i + 4).equalsIgnoreCase('todo') 
|| !s.isComment(i)) {
+            return NONE
+        }
+        int e = i + 4
+        if (e < s.length() && s.charAt(e) == ':' as char) {
+            e++
+        }
+        s.replace(i, e, 'TODO:')
+        i
+    }
+
+    // ---- javadoc 
----------------------------------------------------------------
+
+    // [start, end) of the javadoc comment enclosing off, or null.
+    static int[] javadocBounds(Src s, int off) {
+        int start = s.lastIndexOf('/**', off)
+        if (start < 0) {
+            return null
+        }
+        int end = s.indexOf('*/', start + 3)
+        if (end < 0 || end + 2 < off) {
+            return null
+        }
+        [start, end + 2] as int[]
+    }
+
+    static int javadocAsteriskAlign(Src s, int off, String msg, int[] ls) {
+        Matcher m = msg =~ /expected is (\d+)/
+        if (!m.find()) {
+            return NONE
+        }
+        int lineStart = s.lineStart(off)
+        int fnw = s.firstNonWs(lineStart)
+        if (fnw >= s.length() || s.charAt(fnw) != '*' as char) {
+            return NONE
+        }
+        s.replace(lineStart, fnw, spaces((m.group(1) as int) - 1))
+        lineStart
+    }
+
+    static int javadocMissingAsterisk(Src s, int off, String msg, int[] ls) {
+        int[] b = javadocBounds(s, off)
+        if (b == null) {
+            return NONE
+        }
+        int col = b[0] - s.lineStart(b[0])
+        int lineStart = s.lineStart(off)
+        String t = s.line(off).trim()
+        String fixed = t.startsWith('*/') ? spaces(col + 1) + t : (t.isEmpty() 
? spaces(col + 1) + '*' : spaces(col + 1) + '* ' + t)
+        s.replace(lineStart, s.lineEnd(off), fixed)
+        lineStart
+    }
+
+    static int javadocContentLocation(Src s, int off, String msg, int[] ls) {
+        if (!s.has(off, '/**')) {
+            return NONE
+        }
+        int k = off + 3
+        while (k < s.length() && s.isHorizontalWs(k)) {
+            k++
+        }
+        if (k >= s.length() || s.charAt(k) == '\n' as char || s.has(k, '*/')) {
+            return NONE
+        }
+        int col = off - s.lineStart(off)
+        s.replace(off + 3, k, '\n' + spaces(col + 1) + '* ')
+        off + 3
+    }
+
+    static int requireEmptyLineBeforeTags(Src s, int off, String msg, int[] 
ls) {
+        int lineStart = s.lineStart(off)
+        int fnw = s.firstNonWs(lineStart)
+        if (fnw >= s.length() || s.charAt(fnw) != '*' as char || s.has(fnw, 
'*/')) {
+            return NONE
+        }
+        s.insert(lineStart, s.slice(lineStart, fnw + 1) + '\n')
+        lineStart
+    }
+
+    static int javadocParagraph(Src s, int off, String msg, int[] ls) {
+        if (msg.contains('should be preceded with an empty line')) {
+            int lineStart = s.lineStart(off)
+            int fnw = s.firstNonWs(lineStart)
+            if (fnw >= s.length() || s.charAt(fnw) != '*' as char || 
!s.slice(fnw + 1, off).trim().isEmpty()) {
+                return NONE
+            }
+            s.insert(lineStart, s.slice(lineStart, fnw + 1) + '\n')
+            return lineStart
+        }
+        if (msg.contains('immediately before the first word')) {
+            if (!s.has(off, '<p>')) {
+                return NONE
+            }
+            int k = off + 3
+            while (k < s.length() && s.isHorizontalWs(k)) {
+                k++
+            }
+            if (k < s.length() && s.charAt(k) == '\n' as char) {
+                int nls = k + 1
+                int nfn = s.firstNonWs(nls)
+                if (nfn >= s.length() || s.charAt(nfn) != '*' as char || 
s.has(nfn, '*/')) {
+                    return NONE
+                }
+                int text = nfn + 1
+                while (text < s.length() && s.isHorizontalWs(text)) {
+                    text++
+                }
+                if (text >= s.lineEnd(nls)) {
+                    return NONE
+                }
+                s.delete(off + 3, text)
+                return off + 3
+            }
+            if (k == off + 3) {
+                return NONE
+            }
+            s.delete(off + 3, k)
+            return off + 3
+        }
+        if (msg.contains('should be followed by <p>')) {
+            int nls = s.lineEnd(off) + 1
+            if (nls >= s.length()) {
+                return NONE
+            }
+            int nfn = s.firstNonWs(nls)
+            if (s.charAt(nfn) != '*' as char || s.has(nfn, '*/')) {
+                return NONE
+            }
+            int text = nfn + 1
+            while (text < s.length() && s.isHorizontalWs(text)) {
+                text++
+            }
+            if (text >= s.lineEnd(nls) || s.charAt(text) == '@' as char || 
s.charAt(text) == '<' as char) {
+                return NONE
+            }
+            s.insert(text, '<p>')
+            return text
+        }
+        NONE
+    }
+
+    static int javadocTagContinuation(Src s, int off, String msg, int[] ls) {
+        int lineStart = s.lineStart(off)
+        Matcher m = s.line(off) =~ /^(\s*\*)\s*(\S.*)$/
+        if (!m.matches()) {
+            return NONE
+        }
+        s.replace(lineStart, s.lineEnd(off), m.group(1) + spaces(basicOffset + 
1) + m.group(2))
+        lineStart
+    }
+
+    static int atclauseOrder(Src s, int off, String msg, int[] ls) {
+        int[] b = javadocBounds(s, off)
+        if (b == null) {
+            return NONE
+        }
+        List<String> lines = s.slice(b[0], b[1]).split('\n', -1).toList()
+        if (lines.size() < 3 || lines.last().trim() != '*/') {
+            return NONE
+        }
+        Map<String, Integer> rank = ['@param': 0, '@return': 1, '@throws': 2, 
'@deprecated': 3]
+        int first = lines.findIndexOf {
+            it.replaceFirst(/^\s*\*\s?/, '').startsWith('@')
+        }
+        if (first < 0) {
+            return NONE
+        }
+        List<List<String>> blocks = []
+        List<String> tags = []
+        for (int i = first; i < lines.size() - 1; i++) {
+            String content = lines[i].replaceFirst(/^\s*\*\s?/, '')
+            if (content.startsWith('@')) {
+                    blocks << []
+                    tags << content.split(/[\s{]/)[0]
+            }
+                blocks.last() << lines[i]
+        }
+        List<Integer> slots = (0..<blocks.size()).findAll {
+            rank.containsKey(tags[it])
+        }
+        List<Integer> ordered = slots.sort(false) {
+            rank[tags[it]]
+        }
+        if (slots == ordered) {
+            return NONE
+        }
+        List<List<String>> rebuilt = new ArrayList<>(blocks)
+        for (int k = 0; k < slots.size(); k++) {
+            rebuilt[slots[k]] = blocks[ordered[k]]
+        }
+        List<String> out = lines.subList(0, first) + rebuilt.flatten() + 
[lines.last()]
+        s.replace(b[0], b[1], out.join('\n'))
+        b[0]
+    }
+
+    static int summaryJavadoc(Src s, int off, String msg, int[] ls) {
+        // Checkstyle reports these right after the opening slash-star-star: 
find the first text char
+        while (off < s.length() && (Character.isWhitespace(s.charAt(off))
+                || (s.charAt(off) == '*' as char && !s.has(off, '*/')))) {
+            off++
+        }
+        if (msg.contains('Forbidden summary fragment')) {
+            Matcher m = s.slice(off, Math.min(s.length(), off + 40)) =~ 
/^[a-z]+(?=[\s.,])/
+            if (!m.find() || !s.isComment(off)) {
+                return NONE
+            }
+            s.replace(off, off + 1, s.slice(off, off + 1).toUpperCase())
+            return off
+        }
+        if (msg.contains('missing an ending period')) {
+            int lineStart = s.lineStart(off)
+            int lastEnd = -1
+            int cur = lineStart
+            boolean firstLine = true
+            while (cur <= s.length()) {
+                int le = s.lineEnd(cur)
+                int textStart = firstLine ? off : s.firstNonWs(cur)
+                if (!firstLine && textStart < le && s.charAt(textStart) == '*' 
as char && !s.has(textStart, '*/')) {
+                    textStart++
+                    while (textStart < le && s.isHorizontalWs(textStart)) {
+                        textStart++
+                    }
+                }
+                String content = s.slice(Math.min(textStart, le), le)
+                int close = content.indexOf('*/')
+                if (close >= 0) {
+                    content = content.substring(0, close)
+                }
+                String t = content.trim()
+                if (t.isEmpty() || (!firstLine && (t.startsWith('@') || 
t.startsWith('<p>')))) {
+                    break
+                }
+                lastEnd = textStart + content.replaceAll(/\s+$/, '').length()
+                if (close >= 0 || le >= s.length()) {
+                    break
+                }
+                cur = le + 1
+                firstLine = false
+            }
+            if (lastEnd < 0) {
+                return NONE
+            }
+            char last = s.charAt(lastEnd - 1)
+            if (last == ':' as char || last == '>' as char || last == '.' as 
char) {
+                return NONE
+            }
+            s.insert(lastEnd, '.')
+            return lastEnd
+        }
+        NONE
+    }
+
+    static int invalidJavadocPosition(Src s, int off, String msg, int[] ls) {
+        if (s.has(off, '/**') && !s.has(off, '/**/')) {
+            s.replace(off, off + 3, '/*')
+            return off
+        }
+        NONE
+    }
+
+    // ---- indentation 
------------------------------------------------------------
+
+    // ---- line length 
------------------------------------------------------------
+
+    static int lineLength(Src s, int off, String msg, int[] ls) {
+        int lineStart = s.lineStart(off)
+        int le = s.lineEnd(off)
+        String line = s.slice(lineStart, le)
+        int max = maxLineLength
+        if (line.length() <= max) {
+            return NONE
+        }
+        int fnw = s.firstNonWs(lineStart)
+        if (fnw >= le) {
+            return NONE
+        }
+        if (s.isComment(fnw)) {
+            return wrapComment(s, lineStart, fnw, le, max)
+        }
+        wrapCode(s, lineStart, fnw, le, max)
+    }
+
+    private static int wrapComment(Src s, int lineStart, int fnw, int le, int 
max) {
+        String line = s.slice(lineStart, le)
+        if (line =~ /(https?|ftp):\/\/|href\s*=/) {
+            return NONE
+        }
+        String prefix
+        String content
+        if (s.has(fnw, '//')) {
+            int t = fnw + 2
+            while (t < le && s.isHorizontalWs(t)) {
+                t++
+            }
+            prefix = s.slice(lineStart, fnw) + '// '
+            content = s.slice(t, le).trim()
+        } else if (s.charAt(fnw) == '*' as char && !s.has(fnw, '*/')) {
+            int t = fnw + 1
+            while (t < le && s.isHorizontalWs(t)) {
+                t++
+            }
+            content = s.slice(t, le).trim()
+            if (content.contains('*/') || content.startsWith('<pre') || 
content.contains('{@code')) {
+                return NONE
+            }
+            String extra = ''
+            // continuation of a block tag is indented one extra level; skip 
preformatted text
+            int cur = lineStart
+            while (cur > 0) {
+                int pls = s.lineStart(cur - 1)
+                String pl = s.line(pls).trim().replaceFirst(/^\/?\*+\s?/, '')
+                if (pl.contains('<pre>') && !pl.contains('</pre>')) {
+                    return NONE
+                }
+                if (pl.startsWith('@')) {
+                    extra = spaces(basicOffset)
+                    break
+                }
+                if (pl.isEmpty() || s.line(pls).trim().startsWith('/**') || 
s.line(pls).trim().startsWith('/*')) {
+                    break
+                }
+                cur = pls
+            }
+            if (content.startsWith('@') || !extra.isEmpty()) {
+                extra = spaces(basicOffset)
+            }
+            prefix = s.slice(lineStart, fnw + 1) + ' ' + 
(content.startsWith('@') ? '' : extra)
+            if (content.startsWith('@')) {
+                // wrapped tag lines continue indented
+                prefix = s.slice(lineStart, fnw + 1) + ' ' + 
spaces(basicOffset)
+                return wrapWords(s, lineStart, le, s.slice(lineStart, fnw + 1) 
+ ' ', prefix, content, max)
+            }
+            return wrapWords(s, lineStart, le, s.slice(lineStart, fnw + 1) + ' 
' + extra, prefix, content, max)
+        } else {
+            return NONE
+        }
+        wrapWords(s, lineStart, le, prefix, prefix, content, max)
+    }
+
+    private static int wrapWords(Src s, int lineStart, int le, String 
firstPrefix, String nextPrefix, String content, int max) {
+        List<String> words = content.split(/\s+/).toList()
+        List<String> out = []
+        StringBuilder cur = new StringBuilder(firstPrefix)
+        boolean empty = true
+        for (String w : words) {
+            int add = (empty ? 0 : 1) + w.length()
+            if (!empty && cur.length() + add > max) {
+                    out << cur.toString()
+                cur = new StringBuilder(nextPrefix)
+                empty = true
+                add = w.length()
+            }
+            if (!empty) {
+                cur.append(' ')
+            }
+            cur.append(w)
+            empty = false
+        }
+            out << cur.toString()
+        if (out.size() < 2 || out.any {
+            it.length() > max && it.trim().split(/\s+/).size() > 2
+        }) {
+            if (out.size() < 2) {
+                return NONE
+            }
+        }
+        s.replace(lineStart, le, out.join('\n'))
+        lineStart
+    }
+
+    private static int wrapCode(Src s, int lineStart, int fnw, int le, int 
max) {
+        String head = s.slice(fnw, Math.min(le, fnw + 8))
+        if (head.startsWith('import ') || head.startsWith('package ')) {
+            return NONE
+        }
+        // candidate break offsets, all inside code
+        int best = -1
+        int depth = 0
+        for (int i = fnw; i < le; i++) {
+            if (!s.isCode(i)) {
+                continue
+            }
+            char c = s.charAt(i)
+            if (c == '(' as char || c == '[' as char) {
+                depth++
+            } else if (c == ')' as char || c == ']' as char) {
+                depth--
+            }
+            int pos = -1
+            if (c == ',' as char && depth > 0) {
+                pos = i + 1
+            } else if ((s.has(i, '&&') || s.has(i, '||')) && i > fnw && 
s.isHorizontalWs(i - 1)) {
+                pos = i
+            } else if (c == '+' as char && i > fnw && s.isHorizontalWs(i - 1) 
&& i + 1 < le && s.isHorizontalWs(i + 1)) {
+                pos = i
+            } else if (c == '?' as char && i > fnw && s.isHorizontalWs(i - 1) 
&& i + 1 < le && s.isHorizontalWs(i + 1)) {
+                pos = i
+            } else if (c == '.' as char && i > fnw && i + 1 < le && 
Character.isJavaIdentifierStart(s.charAt(i + 1))
+                    && (s.charAt(i - 1) == ')' as char || 
Character.isJavaIdentifierPart(s.charAt(i - 1)))) {
+                String after = s.slice(i + 1, le)
+                if (after =~ /^\w+\s*\(/) {
+                    pos = i
+                }
+            }
+            if (pos > fnw && pos - lineStart <= max && (pos - lineStart) > 
(fnw - lineStart) + 8) {
+                best = pos
+            }
+        }
+        if (best < 0) {
+            for (int i = fnw; i < le; i++) {
+                if (s.isCode(i) && s.charAt(i) == '=' as char && 
s.isHorizontalWs(i - 1) && i + 1 < le && s.isHorizontalWs(i + 1)
+                        && i + 1 - lineStart <= max && i + 1 > fnw + 8) {
+                    best = i + 1
+                }
+            }
+        }
+        if (best < 0) {
+            return splitString(s, lineStart, fnw, le, max)
+        }
+        int trimEnd = best
+        while (trimEnd > lineStart && s.isHorizontalWs(trimEnd - 1)) {
+            trimEnd--
+        }
+        int restStart = best
+        while (restStart < le && s.isHorizontalWs(restStart)) {
+            restStart++
+        }
+        if (restStart >= le) {
+            return NONE
+        }
+        s.replace(trimEnd, restStart, nl(continuationIndent(s, lineStart, 
fnw)))
+        trimEnd
+    }
+
+    /** Indent for a wrapped continuation: keep an existing continuation's 
indent, else statement indent + 2 levels. */
+    static String continuationIndent(Src s, int lineStart, int fnw) {
+        String indent = s.slice(lineStart, fnw)
+        int p = lineStart > 0 ? s.prevCode(lineStart - 1) : -1
+        boolean statementStart = p < 0 || s.charAt(p) == ';' as char || 
s.charAt(p) == '{' as char
+                || s.charAt(p) == '}' as char
+        statementStart ? indent + spaces(2 * basicOffset) : indent
+    }
+
+    private static int splitString(Src s, int lineStart, int fnw, int le, int 
max) {
+        for (int i = fnw; i < le; i++) {
+            if (!s.isLiteral(i) || s.charAt(i) != '"' as char || (i > 0 && 
s.isLiteral(i - 1))) {
+                continue
+            }
+            int end = literalEnd(s, i)
+            if (end - i < 6 || s.has(i, '"""') || end <= lineStart + max - 1 
|| end > le) {
+                continue
+            }
+            int prev = s.prevCode(i - 1)
+            int next = s.nextCode(end)
+            if (prev < 0 || next < 0) {
+                return NONE
+            }
+            char pc = s.charAt(prev)
+            String pw = Character.isJavaIdentifierPart(pc) ? 
s.slice(Math.max(0, prev - 5), prev + 1) : ''
+            char nc = s.charAt(next)
+            boolean okPrev = pc in ['(', ',', '=', '+', '?', ':', '{'] as 
char[] || pw.endsWith('return') || pw.endsWith('case')
+            boolean okNext = nc in [',', ')', ';', '+', '?', ':', '}'] as 
char[]
+            if (!okPrev || !okNext) {
+                return NONE
+            }
+            // safe split points: boundaries that don't cut an escape sequence
+            List<Integer> safe = []
+            int k = i + 1
+            while (k < end - 1) {
+                if (s.charAt(k) == '\\' as char) {
+                    // a whole escape: unicode, octal (up to three digits, 
\0-\377) or single character
+                    Matcher e = s.slice(k, end) =~ 
/^\\(u+[0-9a-fA-F]{4}|[0-3][0-7]{0,2}|[4-7][0-7]?)/
+                    k += e.find() ? e.end() : 2
+                } else {
+                    k++
+                }
+                safe << k
+            }
+            int limit = lineStart + max - 2
+            int cut = -1
+            for (int cand : safe) {
+                if (cand <= limit && cand > i + 8 && s.charAt(cand - 1) == ' ' 
as char) {
+                    cut = cand
+                }
+            }
+            if (cut < 0) {
+                for (int cand : safe) {
+                    if (cand <= limit && cand > i + 8) {
+                        cut = cand
+                    }
+                }
+            }
+            if (cut < 0 || cut >= end - 1) {
+                return NONE
+            }
+            s.replace(cut, cut, '"' + nl(continuationIndent(s, lineStart, 
fnw)) + '+ "')
+            return cut
+        }
+        NONE
+    }
+}
+
+// 
=================================================================================
+// Script body
+// 
=================================================================================
+
+// 
---------------------------------------------------------------------------------
+// Load the ruleset directly - the same file maven-checkstyle-plugin's 
configLocation uses (root pom).
+// 
---------------------------------------------------------------------------------
+
+String multiModuleDir = System.getProperty('maven.multiModuleProjectDirectory')
+File rulesetFile = new 
File("${multiModuleDir}/storm-checkstyle/src/main/resources/storm/storm_checkstyle.xml")
+if (!rulesetFile.exists()) {
+    println "[checkstyle-autofix] ${project.artifactId}: ruleset 
${rulesetFile} not found, skipping."
+    return
+}
+
+// Checkstyle rulesets always carry a <!DOCTYPE module PUBLIC ...> pointing at 
the
+// public checkstyle DTD. XmlSlurper's default SAXParser rejects any DOCTYPE 
outright
+// (disallow-doctype-decl=true, a blanket XXE hardening default), so it must 
be turned
+// off here - but external entity/DTD fetching stays disabled so this doesn't 
reopen
+// an XXE hole; the doctype is parsed, its external subset never is.
+def xmlSlurper = new XmlSlurper()
+xmlSlurper.setFeature('http://apache.org/xml/features/disallow-doctype-decl', 
false)
+xmlSlurper.setFeature('http://xml.org/sax/features/external-general-entities', 
false)
+xmlSlurper.setFeature('http://xml.org/sax/features/external-parameter-entities',
 false)
+xmlSlurper.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd',
 false)
+def checker = xmlSlurper.parse(rulesetFile)
+def treeWalker = checker.module.find {
+    it.@name == 'TreeWalker'
+}
+
+def findModule = { String name ->
+    treeWalker.module.find {
+        it.@name == name
+    }
+}
+def moduleProperty = { module, String propName, String defaultValue = null ->
+    def prop = module?.property?.find {
+        it.@name == propName
+    }
+    prop ? [email protected]() : defaultValue
+}
+
+Fixers.basicOffset = moduleProperty(findModule('Indentation'), 'basicOffset', 
'4').toInteger()
+Fixers.maxLineLength = moduleProperty(checker.module.find {
+    it.@name == 'LineLength'
+}, 'max', '100').toInteger()
+
+
+def sourceRoots = ((project.compileSourceRoots ?: []) + 
(project.testCompileSourceRoots ?: [])).unique()
+// Honour the same <excludes> the module gives maven-checkstyle-plugin (e.g. 
thrift-generated
+// code): those files aren't checked, so they must not be rewritten - or even 
audited, which
+// for the huge generated classes is very slow.
+def checkstylePlugin = project.build?.plugins?.find {
+    it.artifactId == 'maven-checkstyle-plugin'
+}
+List<java.nio.file.PathMatcher> excludeMatchers = 
(checkstylePlugin?.configuration?.getChild('excludes')?.value ?: '')
+        .split(',').collect {
+    it.trim()
+}.findAll {
+    it
+}
+        .collect {
+            
java.nio.file.FileSystems.default.getPathMatcher("glob:/${it}".toString())
+        }
+List<File> javaFiles = []
+sourceRoots.each { rootPath ->
+    File rootDir = new File(rootPath)
+    if (rootDir.exists()) {
+        rootDir.eachFileRecurse(FileType.FILES) { file ->
+            String relative = '/' + 
rootDir.toPath().relativize(file.toPath()).toString().replace(File.separator, 
'/')
+            if (file.name.endsWith('.java') && !excludeMatchers.any {
+                it.matches(java.nio.file.Paths.get(relative))
+            }) {
+                    javaFiles << file
+            }
+        }
+    }
+}
+
+// Stage 2: audit with Checkstyle, fix what each reported violation points at, 
repeat.
+Map<String, Closure> fixers = [
+        OneStatementPerLine               : Fixers.&oneStatementPerLine,
+        LineLength                        : Fixers.&lineLength,
+        MatchXpath                        : Fixers.&commentSpace,
+        SeparatorWrap                     : Fixers.&separatorWrap,
+        OverloadMethodsDeclarationOrder   : Fixers.&overloadOrder,
+        ConstructorsDeclarationGrouping   : Fixers.&constructorGrouping,
+        IllegalTokenText                  : Fixers.&illegalTokenText,
+        AvoidEscapedUnicodeCharacters     : Fixers.&avoidEscapedUnicode,
+        TodoComment                       : Fixers.&todoComment,
+        JavadocLeadingAsteriskAlign       : Fixers.&javadocAsteriskAlign,
+        JavadocMissingLeadingAsterisk     : Fixers.&javadocMissingAsterisk,
+        JavadocContentLocation            : Fixers.&javadocContentLocation,
+        JavadocParagraph                  : Fixers.&javadocParagraph,
+        JavadocTagContinuationIndentation : Fixers.&javadocTagContinuation,
+        RequireEmptyLineBeforeBlockTagGroup: 
Fixers.&requireEmptyLineBeforeTags,
+        AtclauseOrder                     : Fixers.&atclauseOrder,
+        SummaryJavadoc                    : Fixers.&summaryJavadoc,
+        InvalidJavadocPosition            : Fixers.&invalidJavadocPosition,
+]
+
+final int MAX_ROUNDS = 25
+
+Map<String, List<Map>> violations = [:]
+Set<String> unparsable = [] as Set
+
+Properties checkstyleProps = new Properties()
+checkstyleProps.setProperty('org.checkstyle.google.severity', 'error')
+def config = ConfigurationLoader.loadConfiguration(rulesetFile.absolutePath, 
new PropertiesExpander(checkstyleProps))
+def auditor = new Checker()
+auditor.setModuleClassLoader(Checker.classLoader)
+// columns then equal char offsets + 1 even if a tab slips through
+auditor.setTabWidth(1)
+auditor.configure(config)
+auditor.addListener([
+        auditStarted : { AuditEvent e ->
+        },
+        auditFinished: { AuditEvent e ->
+        },
+        fileStarted  : { AuditEvent e ->
+        },
+        fileFinished : { AuditEvent e ->
+        },
+        addError     : { AuditEvent e ->
+            String key = e.sourceName.substring(e.sourceName.lastIndexOf('.') 
+ 1).replaceFirst(/Check$/, '')
+            violations.get(e.fileName, []) << [line: e.line, col: e.column, 
msg: e.message, check: key]
+        },
+        addException : { AuditEvent e, Throwable t ->
+            unparsable << e.fileName
+        },
+] as AuditListener)
+
+// One file per call: Checkstyle throws out of process() on a file that 
doesn't parse.
+def audit = { List<File> files ->
+    violations.clear()
+    unparsable.clear()
+    files.each { File f ->
+        try {
+            auditor.process([f])
+        } catch (Exception ex) {
+                unparsable << f.absolutePath
+        }
+    }
+}
+
+Map<File, String> originalText = [:]
+Map<File, String> beforeRound = [:]
+Map<File, Integer> initialCount = [:]
+Map<File, Integer> latestCount = [:]
+Map<String, Integer> appliedByCheck = [:].withDefault {
+    0
+}
+Set<File> frozen = [] as Set
+Map<String, String> fixerErrors = [:]
+
+// skip CRLF files: the fixers write '\n'
+List<File> pending = javaFiles.findAll { File f ->
+    !f.getText('UTF-8').contains('\r')
+}
+int round = 0
+while (!pending.isEmpty() && round < MAX_ROUNDS) {
+    round++
+    pending.each { File f ->
+        originalText.putIfAbsent(f, f.getText('UTF-8'))
+    }
+    audit(pending)
+    List<File> next = []
+    pending.each { File f ->
+        List<Map> vs = violations[f.absolutePath] ?: []
+        latestCount[f] = vs.size()
+        if (!initialCount.containsKey(f)) {
+            initialCount[f] = vs.size()
+        }
+        if (unparsable.contains(f.absolutePath)) {
+            if (beforeRound.containsKey(f)) {
+                f.setText(beforeRound[f], 'UTF-8')
+                    frozen << f
+                println "[checkstyle-autofix] ${f}: a fix left the file 
unparsable; reverted the last round."
+            }
+            return
+        }
+        String text = f.getText('UTF-8')
+        List<Map> fixable = vs.findAll {
+            fixers.containsKey(it.check)
+        }
+        if (fixable.isEmpty()) {
+            return
+        }
+        int[] lineStarts = lineStartsOf(text)
+        Src src = new Src(text)
+        int headerEnd = licenseHeaderEnd(text)
+        List<Map> work = fixable.collect { Map v ->
+            v + [off: v.line >= 1 && v.line <= lineStarts.length ? 
lineStarts[v.line - 1] + Math.max(0, v.col - 1) : -1]
+        }.findAll {
+            // nothing reported inside the license header is fixed
+            it.off >= headerEnd
+        }
+        // last-to-first; on ties handle the token's trailing side first
+        work.sort { a, b ->
+            b.off <=> a.off
+        }
+        int lowest = Integer.MAX_VALUE
+        work.each { Map v ->
+            if (v.off >= lowest) {
+                return
+            }
+            int touched
+            try {
+                touched = (fixers[v.check].call(src, v.off, v.msg, lineStarts) 
as int)
+            } catch (Exception ex) {
+                fixerErrors[v.check] = "${ex.class.simpleName}: 
${ex.message}".toString()
+                touched = Fixers.NONE
+            }
+            if (touched >= 0) {
+                lowest = Math.min(lowest, touched)
+                appliedByCheck[v.check]++
+            }
+        }
+        String updated = src.toString()
+        if (updated != text) {
+            beforeRound[f] = text
+            f.setText(updated, 'UTF-8')
+                next << f
+        }
+    }
+    pending = next.findAll {
+        !frozen.contains(it)
+    }
+}
+if (!pending.isEmpty()) {
+    audit(pending)
+    pending.each { File f ->
+        latestCount[f] = (violations[f.absolutePath] ?: []).size()
+    }
+}
+
+// A file that ended with more violations than it started with is put back as 
it was.
+int filesChanged = 0
+originalText.each { File f, String original ->
+    if (f.getText('UTF-8') == original) {
+        return
+    }
+    if (latestCount[f] != null && initialCount[f] != null && latestCount[f] > 
initialCount[f]) {
+        f.setText(original, 'UTF-8')
+        println "[checkstyle-autofix] ${f}: fixing made it worse 
(${initialCount[f]} -> ${latestCount[f]} violations); restored."
+    } else {
+        filesChanged++
+    }
+}
+auditor.destroy()
+
+println "[checkstyle-autofix] ${project.artifactId}: " +
+        "fixes applied ${appliedByCheck.sort()} over ${round} round(s), 
${filesChanged} file(s) changed, " +
+        "ruleset=storm_checkstyle.xml." + (fixerErrors ? " Fixer errors (bug 
in the fixer, violation left as is): ${fixerErrors}" : '')
+
+/** End of the license comment at the top of the file, 0 if the file doesn't 
start with one. */
+static int licenseHeaderEnd(String text) {
+    Matcher m = text =~ /(?s)\A\s*\/\*.*?\*\//
+    m.find() && m.group().contains('License') ? m.end() : 0
+}
+
+static int[] lineStartsOf(String text) {
+    List<Integer> starts = [0]
+    int i = text.indexOf('\n')
+    while (i >= 0) {
+            starts << i + 1
+        i = text.indexOf('\n', i + 1)
+    }
+    starts as int[]
+}
diff --git 
a/storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy 
b/storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy
new file mode 100644
index 000000000..10f4a1c51
--- /dev/null
+++ b/storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy
@@ -0,0 +1,176 @@
+/*
+ * 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
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Rewrites the leading license comment of the files of a module (Java, 
Groovy, XML, Markdown,
+ * YAML, properties, Python and shell) to the exact template of
+ * https://www.apache.org/legal/src-headers.html#headers.
+ *
+ * A file is left alone when its leading comment is not an ASF license header, 
when it carries
+ * a copyright notice of anybody but the ASF (third-party works keep their own 
notice), or when its
+ * header already has the words of the template, whatever the line wrapping, 
the case (the older
+ * 'to You' form) or the Javadoc '<p>' an IDE formatter adds: rewrapping such 
a header would touch
+ * most files of the code base and pollute git blame for no legal gain. So 
only headers whose
+ * wording is wrong (typos, missing words) are rewritten.
+ *
+ * Only the license lines are replaced: a block or XML comment with anything 
besides the license is
+ * left alone, and the '#' lines around the license (e.g. a '-*- coding -*-' 
line) are kept.
+ *
+ * Run on demand via gmavenplus-plugin's `execute` goal (execution id 
`normalize-license-headers`,
+ * bound to no phase); see storm-checkstyle/README.md. With 
-Dlicense.check=true nothing is
+ * written and the build fails if a file would change.
+ */
+
+final List<String> TEXT = '''\
+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
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.'''.readLines()
+
+boolean checkOnly = Boolean.getBoolean('license.check')
+
+// the words of a comment, without comment markers and line wrapping
+String words(String comment) {
+    return comment.readLines()
+        .collect { it.replaceAll(/\*\/|-->|<p\s*\/?>/, 
'').replaceFirst(/^\s*(\/\*+|\*|#|<!--|~)?/, '') }
+        .join(' ').split(/\s+/).findAll().join(' ')
+}
+
+templateWords = words(TEXT.join('\n')).toLowerCase()
+
+blockHeader = '/*\n' + TEXT.collect { it.isEmpty() ? ' *' : ' * ' + it 
}.join('\n') + '\n */\n'
+hashHeader = TEXT.collect { it.isEmpty() ? '#' : '# ' + it }.join('\n') + '\n'
+xmlHeader = '<!--\n' + TEXT.collect { it.isEmpty() ? '' : ' ' + it 
}.join('\n') + '\n-->\n'
+
+boolean isAsfLicense(String comment) {
+    if (!comment.contains('Apache Software Foundation') || 
!(comment.contains('Licensed to') || comment.contains('Licensed under'))) {
+        return false
+    }
+    // a copyright line of somebody else: a third-party work
+    return !comment.readLines().any { it.contains('Copyright') && 
!it.contains('Apache Software Foundation') }
+}
+
+// a comment holding the license and nothing else, and not already worded as 
the template
+boolean needsRewrite(String comment) {
+    String w = words(comment)
+    return isAsfLicense(comment) && w.startsWith('Licensed ') && 
w.endsWith('under the License.') && w.toLowerCase() != templateWords
+}
+
+// '/* */' comment at the top of the file
+String fixBlock(String text, String header) {
+    def m = (text =~ /(?s)\A\s*\/\*.*?\*\/[ \t]*\n?/)
+    if (!m.find() || !needsRewrite(m.group())) {
+        return text
+    }
+    return header + '\n' + text.substring(m.end()).replaceFirst(/^\n+/, '')
+}
+
+// '#' comment lines at the top, after an optional shebang: only the license 
lines are replaced
+String fixHash(String text, String header) {
+    String prefix = ''
+    String rest = text
+    if (rest.startsWith('#!')) {
+        int nl = rest.indexOf('\n')
+        prefix = rest.substring(0, nl + 1)
+        rest = rest.substring(nl + 1)
+    }
+    def m = (rest =~ /\A(\s*)((?:[ \t]*#[^\n]*(?:\n|\z))+)/)
+    if (!m.find()) {
+        return text
+    }
+    // the license runs from the 'Licensed ...' line to the '... under the 
License.' line
+    List<String> lines = m.group(2).readLines()
+    int start = lines.findIndexOf { it =~ /Licensed (to|under)/ }
+    int end = start < 0 ? -1 : lines.findIndexOf(start) { it.contains('under 
the License.') }
+    if (end < 0 || !needsRewrite(lines[start..end].join('\n'))) {
+        return text
+    }
+    List<String> run = lines.subList(0, start) + header.readLines() + 
lines.subList(end + 1, lines.size())
+    return prefix + m.group(1) + run.join('\n') + (m.group(2).endsWith('\n') ? 
'\n' : '') + rest.substring(m.end())
+}
+
+// '<!-- -->' comment at the top, after an optional '<?xml ...?>' declaration
+String fixXml(String text, String header) {
+    String prefix = ''
+    String rest = text
+    def decl = (rest =~ /\A<\?xml[^>]*\?>[ \t]*\n/)
+    if (decl.find()) {
+        prefix = decl.group()
+        rest = rest.substring(decl.end()).replaceFirst(/^\n+/, '')
+    }
+    def m = (rest =~ /(?s)\A\s*<!--.*?-->[ \t]*\n?/)
+    if (!m.find() || !needsRewrite(m.group())) {
+        return text
+    }
+    String after = rest.substring(m.end()).replaceFirst(/^\n+/, '')
+    return prefix + header + (after ? '\n' + after : '')
+}
+
+Closure fixerFor(String name) {
+    if (name.endsWith('.java') || name.endsWith('.groovy')) {
+        return { String t -> fixBlock(t, blockHeader) }
+    }
+    if (name.endsWith('.xml') || name.endsWith('.md')) {
+        return { String t -> fixXml(t, xmlHeader) }
+    }
+    if (['.yaml', '.yml', '.properties', '.py', '.sh'].any { name.endsWith(it) 
}) {
+        return { String t -> fixHash(t, hashHeader) }
+    }
+    return null
+}
+
+Set<String> skippedDirs = ['target', 'node_modules', 'generated', '.git'] as 
Set
+List<String> changed = []
+File base = project.basedir
+base.eachFileRecurse(groovy.io.FileType.FILES) { File f ->
+    String rel = base.toPath().relativize(f.toPath()).toString()
+    if (rel.split('/').any { skippedDirs.contains(it) }) {
+        return
+    }
+    Closure fix = fixerFor(f.name)
+    if (fix == null) {
+        return
+    }
+    String text = f.getText('UTF-8')
+    String fixed = fix(text)
+    if (fixed != text) {
+        changed << rel
+        if (!checkOnly) {
+            f.setText(fixed, 'UTF-8')
+        }
+    }
+}
+
+println "[normalize-license-headers] ${project.artifactId}: ${changed.size()} 
file(s) ${checkOnly ? 'need a header fix' : 'rewritten'}"
+if (checkOnly && !changed.isEmpty()) {
+    changed.each { println "  ${it}" }
+    throw new IllegalStateException('License headers differ from the ASF 
template')
+}
diff --git a/storm-checkstyle/src/main/resources/storm/storm_checkstyle.xml 
b/storm-checkstyle/src/main/resources/storm/storm_checkstyle.xml
index ab9265d1e..006741a7b 100644
--- a/storm-checkstyle/src/main/resources/storm/storm_checkstyle.xml
+++ b/storm-checkstyle/src/main/resources/storm/storm_checkstyle.xml
@@ -64,6 +64,15 @@
         <property name="lineRange" value="$2"/>
     </module>
 
+    <!-- Both wrappings of the ASF license header are accepted; the Javadoc 
one ('/**') trips
+         JavadocParagraph on its blank lines, so the check is muted on the 
header only: from the
+         header's last text line (ending in 'under the License.') back to the 
top of the file. -->
+    <module name="SuppressWithNearbyTextFilter">
+        <property name="nearbyTextPattern" value="^ \* (.* )?under the 
License\.$"/>
+        <property name="checkPattern" value="JavadocParagraph"/>
+        <property name="lineRange" value="-20"/>
+    </module>
+
     <!-- Checks for whitespace                               -->
     <!-- See http://checkstyle.org/checks/whitespace/index.html -->
     <module name="FileTabCharacter">
diff --git a/storm-client/pom.xml b/storm-client/pom.xml
index cfa566830..a97f712da 100644
--- a/storm-client/pom.xml
+++ b/storm-client/pom.xml
@@ -199,6 +199,16 @@
                     
<reportsDirectory>${project.build.directory}/test-reports</reportsDirectory>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-core/pom.xml b/storm-core/pom.xml
index 1f0ef1d74..3840e05d5 100644
--- a/storm-core/pom.xml
+++ b/storm-core/pom.xml
@@ -233,6 +233,16 @@
             </testResource>
         </testResources>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-multilang/javascript/pom.xml 
b/storm-multilang/javascript/pom.xml
index edff4460c..c063db86a 100644
--- a/storm-multilang/javascript/pom.xml
+++ b/storm-multilang/javascript/pom.xml
@@ -33,6 +33,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-multilang/ruby/pom.xml b/storm-multilang/ruby/pom.xml
index 291a3cdcd..ae84aee31 100644
--- a/storm-multilang/ruby/pom.xml
+++ b/storm-multilang/ruby/pom.xml
@@ -33,6 +33,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-server/pom.xml b/storm-server/pom.xml
index ae859c3f5..b1ec5c8f8 100644
--- a/storm-server/pom.xml
+++ b/storm-server/pom.xml
@@ -198,6 +198,16 @@
                     
<reportsDirectory>${project.build.directory}/test-reports</reportsDirectory>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-submit-tools/pom.xml b/storm-submit-tools/pom.xml
index c3e2a7737..ea65fc86a 100644
--- a/storm-submit-tools/pom.xml
+++ b/storm-submit-tools/pom.xml
@@ -112,6 +112,16 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
diff --git a/storm-webapp/pom.xml b/storm-webapp/pom.xml
index 20a0c48aa..4f6e5d0ac 100644
--- a/storm-webapp/pom.xml
+++ b/storm-webapp/pom.xml
@@ -249,6 +249,16 @@
                     
<reportsDirectory>${project.build.directory}/test-reports</reportsDirectory>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.openrewrite.maven</groupId>
+                <artifactId>rewrite-maven-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <!--Note - the version/config would be inherited-->
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>

Reply via email to