diff --git a/.github/workflows/checkstyle-autofix.yaml b/.github/workflows/checkstyle-autofix.yaml new file mode 100644 index 0000000000..fb827189f8 --- /dev/null +++ b/.github/workflows/checkstyle-autofix.yaml @@ -0,0 +1,82 @@ +# 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 and pushes the result to +# the branch checkstyle-fix/, from which a pull request against master can be opened. +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 + +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: 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: 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: Push the result to checkstyle-fix/ + run: | + BRANCH="checkstyle-fix/$MODULE" + # 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" + 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 --force origin "$BRANCH" + echo "Pushed $BRANCH; open a pull request against master from it." >> "$GITHUB_STEP_SUMMARY" diff --git a/.mvn/.gitkeep b/.mvn/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..3d61a1f27e --- /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/examples/storm-hdfs-examples/pom.xml b/examples/storm-hdfs-examples/pom.xml index 40e66a2a58..a307844ef2 100644 --- a/examples/storm-hdfs-examples/pom.xml +++ b/examples/storm-hdfs-examples/pom.xml @@ -93,6 +93,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-jdbc-examples/pom.xml b/examples/storm-jdbc-examples/pom.xml index 95525a3cc2..5a3e7ab82e 100644 --- a/examples/storm-jdbc-examples/pom.xml +++ b/examples/storm-jdbc-examples/pom.xml @@ -85,6 +85,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-jms-examples/pom.xml b/examples/storm-jms-examples/pom.xml index 41cf4b9bd7..47f765b7fe 100644 --- a/examples/storm-jms-examples/pom.xml +++ b/examples/storm-jms-examples/pom.xml @@ -105,6 +105,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-kafka-client-examples/pom.xml b/examples/storm-kafka-client-examples/pom.xml index 4700be7714..f29819951e 100644 --- a/examples/storm-kafka-client-examples/pom.xml +++ b/examples/storm-kafka-client-examples/pom.xml @@ -103,6 +103,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-loadgen/pom.xml b/examples/storm-loadgen/pom.xml index 0ae6a3b99a..16080974fd 100644 --- a/examples/storm-loadgen/pom.xml +++ b/examples/storm-loadgen/pom.xml @@ -122,6 +122,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-perf/pom.xml b/examples/storm-perf/pom.xml index 7e9377cbf1..69c435b9e2 100644 --- a/examples/storm-perf/pom.xml +++ b/examples/storm-perf/pom.xml @@ -86,6 +86,16 @@ ${storm.topology} + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-redis-examples/pom.xml b/examples/storm-redis-examples/pom.xml index 68db938f64..662f5e2a36 100644 --- a/examples/storm-redis-examples/pom.xml +++ b/examples/storm-redis-examples/pom.xml @@ -89,6 +89,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/examples/storm-starter/pom.xml b/examples/storm-starter/pom.xml index f7632397f7..b6dc1e66c3 100644 --- a/examples/storm-starter/pom.xml +++ b/examples/storm-starter/pom.xml @@ -174,6 +174,16 @@ none + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-autocreds/pom.xml b/external/storm-autocreds/pom.xml index 80ecdc4f1b..bdf1c3cff8 100644 --- a/external/storm-autocreds/pom.xml +++ b/external/storm-autocreds/pom.xml @@ -112,6 +112,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-blobstore-migration/pom.xml b/external/storm-blobstore-migration/pom.xml index ae3bfa3b8a..9150aa0a22 100644 --- a/external/storm-blobstore-migration/pom.xml +++ b/external/storm-blobstore-migration/pom.xml @@ -146,6 +146,16 @@ limitations under the License. + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-hdfs-blobstore/pom.xml b/external/storm-hdfs-blobstore/pom.xml index a33bbca96e..fe31d7fabe 100644 --- a/external/storm-hdfs-blobstore/pom.xml +++ b/external/storm-hdfs-blobstore/pom.xml @@ -153,6 +153,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-hdfs-oci/pom.xml b/external/storm-hdfs-oci/pom.xml index 73b3f0c95f..e0d4990a92 100644 --- a/external/storm-hdfs-oci/pom.xml +++ b/external/storm-hdfs-oci/pom.xml @@ -104,6 +104,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-hdfs/pom.xml b/external/storm-hdfs/pom.xml index ca1207cfb5..565ddd0e62 100644 --- a/external/storm-hdfs/pom.xml +++ b/external/storm-hdfs/pom.xml @@ -145,6 +145,16 @@ 1 + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-jdbc/pom.xml b/external/storm-jdbc/pom.xml index 770754154c..4816832ef0 100644 --- a/external/storm-jdbc/pom.xml +++ b/external/storm-jdbc/pom.xml @@ -77,6 +77,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-jms/pom.xml b/external/storm-jms/pom.xml index 5aa3fc2abd..dfd61417e0 100644 --- a/external/storm-jms/pom.xml +++ b/external/storm-jms/pom.xml @@ -80,6 +80,16 @@ -Xdoclint:none + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-kafka-client/pom.xml b/external/storm-kafka-client/pom.xml index 368c3dce55..b47b75e3af 100644 --- a/external/storm-kafka-client/pom.xml +++ b/external/storm-kafka-client/pom.xml @@ -173,6 +173,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-kafka-migration/pom.xml b/external/storm-kafka-migration/pom.xml index 1a0f1e580b..f2bcde4ba9 100644 --- a/external/storm-kafka-migration/pom.xml +++ b/external/storm-kafka-migration/pom.xml @@ -81,6 +81,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-kafka-monitor/pom.xml b/external/storm-kafka-monitor/pom.xml index 326e00a34e..0aa34b473e 100644 --- a/external/storm-kafka-monitor/pom.xml +++ b/external/storm-kafka-monitor/pom.xml @@ -81,6 +81,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-metrics/pom.xml b/external/storm-metrics/pom.xml index 31197865bd..da14dbce23 100644 --- a/external/storm-metrics/pom.xml +++ b/external/storm-metrics/pom.xml @@ -99,6 +99,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/external/storm-redis/pom.xml b/external/storm-redis/pom.xml index 0c240cea90..9a380b24b4 100644 --- a/external/storm-redis/pom.xml +++ b/external/storm-redis/pom.xml @@ -100,6 +100,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/flux/flux-core/pom.xml b/flux/flux-core/pom.xml index 3f5b9957a4..2fcb43fa43 100644 --- a/flux/flux-core/pom.xml +++ b/flux/flux-core/pom.xml @@ -114,6 +114,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/flux/flux-examples/pom.xml b/flux/flux-examples/pom.xml index 6cfaa7e3f6..bc3746189a 100644 --- a/flux/flux-examples/pom.xml +++ b/flux/flux-examples/pom.xml @@ -121,6 +121,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/flux/flux-wrappers/pom.xml b/flux/flux-wrappers/pom.xml index 0b69c56727..635daa7884 100644 --- a/flux/flux-wrappers/pom.xml +++ b/flux/flux-wrappers/pom.xml @@ -52,6 +52,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/flux/pom.xml b/flux/pom.xml index fc17125731..098290f12b 100644 --- a/flux/pom.xml +++ b/flux/pom.xml @@ -60,6 +60,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/integration-test/pom.xml b/integration-test/pom.xml index 6878b1bae0..b8e70be8e4 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -164,6 +164,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/pom.xml b/pom.xml index 48347f09a5..0eaf0d202d 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,11 @@ 25 + 4.2.0 + 6.46.1 + 2.41.1 + 14.0.0 + 4.0.29 false false @@ -1110,6 +1115,138 @@ + + + org.openrewrite.maven + rewrite-maven-plugin + ${rewrite-maven-plugin.version} + + + org.apache.storm.checkstyle.AutoFix + + + + + + **/generated/** + + + false + false + + + + org.openrewrite.recipe + rewrite-static-analysis + ${rewrite-static-analysis.version} + + + + + checkstyle-autofix-openrewrite + + none + + run + + + + checkstyle-reindent-openrewrite + + none + + run + + + + org.apache.storm.checkstyle.Reindent + + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + ${gmavenplus.version} + + + org.apache.groovy + groovy + ${groovy.version} + + + + org.apache.groovy + groovy-xml + ${groovy.version} + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + org.apache.maven.doxia + * + + + + + + commons-logging + commons-logging + ${commons-logging.version} + + + + + checkstyle-autofix + + none + + execute + + + + + + + + + normalize-license-headers + + none + + execute + + + + + + + + + org.apache.maven.plugins @@ -1126,7 +1263,7 @@ checkstyle - 14.0.0 + ${checkstyle.version} diff --git a/rewrite.yml b/rewrite.yml new file mode 100644 index 0000000000..727881b27e --- /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 + - + - 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 0000000000..d086832313 --- /dev/null +++ b/storm-checkstyle/README.md @@ -0,0 +1,124 @@ +# 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 (`` of `maven-checkstyle-plugin`, e.g. thrift +generated code) are neither audited nor rewritten. + +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. 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: + +```sh +mvn -pl 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' +``` + +### 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 0000000000..dda2eedd9b --- /dev/null +++ b/storm-checkstyle/src/main/resources/storm/checkstyle-autofix.groovy @@ -0,0 +1,1610 @@ +/* + * 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. + * + * 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 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 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 =~ /(? 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, '

')) { + 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

')) { + 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, '

') + 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 lines = s.slice(b[0], b[1]).split('\n', -1).toList() + if (lines.size() < 3 || lines.last().trim() != '*/') { + return NONE + } + Map rank = ['@param': 0, '@return': 1, '@throws': 2, '@deprecated': 3] + int first = lines.findIndexOf { + it.replaceFirst(/^\s*\*\s?/, '').startsWith('@') + } + if (first < 0) { + return NONE + } + List> blocks = [] + List 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 slots = (0.. ordered = slots.sort(false) { + rank[tags[it]] + } + if (slots == ordered) { + return NONE + } + List> rebuilt = new ArrayList<>(blocks) + for (int k = 0; k < slots.size(); k++) { + rebuilt[slots[k]] = blocks[ordered[k]] + } + List 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('

')))) { + 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(' 0) { + int pls = s.lineStart(cur - 1) + String pl = s.line(pls).trim().replaceFirst(/^\/?\*+\s?/, '') + if (pl.contains('

') && !pl.contains('
')) { + 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 words = content.split(/\s+/).toList() + List 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 safe = [] + int k = i + 1 + while (k < end - 1) { + if (s.charAt(k) == '\\' as char) { + Matcher u = s.slice(k, end) =~ /^\\u+[0-9a-fA-F]{4}/ + k += u.find() ? u.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 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 ? prop.@value.text() : 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 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 excludeMatchers = (checkstylePlugin?.configuration?.getChild('excludes')?.value ?: '') + .split(',').collect { + it.trim() +}.findAll { + it +} + .collect { + java.nio.file.FileSystems.default.getPathMatcher("glob:/${it}".toString()) + } +List 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 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> violations = [:] +Set 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 files -> + violations.clear() + unparsable.clear() + files.each { File f -> + try { + auditor.process([f]) + } catch (Exception ex) { + unparsable << f.absolutePath + } + } +} + +Map originalText = [:] +Map beforeRound = [:] +Map initialCount = [:] +Map latestCount = [:] +Map appliedByCheck = [:].withDefault { + 0 +} +Set frozen = [] as Set +Map fixerErrors = [:] + +// skip CRLF files: the fixers write '\n' +List 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 next = [] + pending.each { File f -> + List 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 fixable = vs.findAll { + fixers.containsKey(it.check) + } + if (fixable.isEmpty()) { + return + } + int[] lineStarts = lineStartsOf(text) + Src src = new Src(text) + List 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 { + it.off >= 0 + } + // 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}" : '') + +static int[] lineStartsOf(String text) { + List 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 0000000000..28233e229c --- /dev/null +++ b/storm-checkstyle/src/main/resources/storm/normalize-license-headers.groovy @@ -0,0 +1,147 @@ +/* + * 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, or when it carries + * a copyright notice of anybody but the ASF (third-party works keep their own notice). + * + * 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 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') + +blockHeader = '/*\n' + TEXT.collect { it.isEmpty() ? ' *' : ' * ' + it }.join('\n') + '\n */\n' +hashHeader = TEXT.collect { it.isEmpty() ? '#' : '# ' + it }.join('\n') + '\n' +xmlHeader = '\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') } +} + +// '/* */' comment at the top of the file +String fixBlock(String text, String header) { + def m = (text =~ /(?s)\A\s*\/\*.*?\*\/[ \t]*\n?/) + if (!m.find() || !isAsfLicense(m.group())) { + return text + } + return header + '\n' + text.substring(m.end()).replaceFirst(/^\n+/, '') +} + +// '#' comment lines at the top, after an optional shebang +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).replaceFirst(/^\n+/, '') + } + def m = (rest =~ /\A(?:[ \t]*#[^\n]*(?:\n|\z))+/) + if (!m.find() || !isAsfLicense(m.group())) { + return text + } + String after = rest.substring(m.end()).replaceFirst(/^\n+/, '') + return prefix + (prefix ? '\n' : '') + header + (after ? '\n' + after : '') +} + +// '' comment at the top, after an optional '' 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() || !isAsfLicense(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 skippedDirs = ['target', 'node_modules', 'generated', '.git'] as Set +List 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-client/pom.xml b/storm-client/pom.xml index cfa5668308..a97f712da1 100644 --- a/storm-client/pom.xml +++ b/storm-client/pom.xml @@ -199,6 +199,16 @@ ${project.build.directory}/test-reports
+ + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/storm-core/pom.xml b/storm-core/pom.xml index 1f0ef1d742..3840e05d5d 100644 --- a/storm-core/pom.xml +++ b/storm-core/pom.xml @@ -233,6 +233,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/storm-multilang/javascript/pom.xml b/storm-multilang/javascript/pom.xml index edff4460c6..c063db86a4 100644 --- a/storm-multilang/javascript/pom.xml +++ b/storm-multilang/javascript/pom.xml @@ -33,6 +33,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/storm-multilang/ruby/pom.xml b/storm-multilang/ruby/pom.xml index 291a3cdcda..ae84aee311 100644 --- a/storm-multilang/ruby/pom.xml +++ b/storm-multilang/ruby/pom.xml @@ -33,6 +33,16 @@ + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/storm-server/pom.xml b/storm-server/pom.xml index ae859c3f5f..b1ec5c8f83 100644 --- a/storm-server/pom.xml +++ b/storm-server/pom.xml @@ -198,6 +198,16 @@ ${project.build.directory}/test-reports + + org.openrewrite.maven + rewrite-maven-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + org.apache.maven.plugins maven-checkstyle-plugin