Simplify creation of a custom ruleset (#3118)

* Simplify and document the build file so that it is more convenient to be used to start a new project for a custom ruleset
* Modify the ktlint version in the build file during the release process
* Improve documentation
* Remove outdated comment in NoVarRuleTest.kt

Closes #3048
diff --git a/.announce b/.announce
index 9f5870e..5f22380 100755
--- a/.announce
+++ b/.announce
@@ -29,6 +29,7 @@
 DOCUMENTATION_DIR="documentation"
 RELEASE_DOCS_DIR="${DOCUMENTATION_DIR}/release-latest"
 SNAPSHOT_DOCS_DIR="${DOCUMENTATION_DIR}/snapshot"
+KTLINT_RULESET_TEMPLATE_DIR="ktlint-ruleset-template"
 
 if [ "$(git status --porcelain=v1 $DOCUMENTATION)" != "" ]; then
   echo "ERROR: To proceed, the current branch must not contain uncommitted changes in directory '${DOCUMENTATION_DIR}'"
@@ -80,6 +81,7 @@
 #   "can't read s/0.49.0/0.49.1/g: No such file or directory"
 sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/cli.md
 sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/integrations.md
+sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" ${KTLINT_RULESET_TEMPLATE_DIR}/build.gradle.kts
 git --no-pager diff ${DOCUMENTATION_DIR}
 
 # ask for user confirmation before committing
diff --git a/documentation/release-latest/docs/api/custom-rule-set.md b/documentation/release-latest/docs/api/custom-rule-set.md
index 83156fa..7a66cb6 100644
--- a/documentation/release-latest/docs/api/custom-rule-set.md
+++ b/documentation/release-latest/docs/api/custom-rule-set.md
@@ -1,17 +1,49 @@
-!!! Tip
-    See [Writing your first ktlint rule](https://medium.com/@vanniktech/writing-your-first-ktlint-rule-5a1707f4ca5b) by [Niklas Baudy](https://github.com/vanniktech).
+You can provide custom rules via a separate ruleset to Ktlint. A ruleset is a JAR containing one or more [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s.
 
-In a nutshell: a "rule set" is a JAR containing one or more [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s. `ktlint` is relying on the [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to discover all available "RuleSet"s on the classpath. As a ruleset author, all you need to do is to include a `META-INF/services/RuleSetProviderV3` file containing a fully qualified name of your [RuleSetProviderV3](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-ruleset-core/src/main/kotlin/com/pinterest/ktlint/cli/ruleset/core/api/RuleSetProviderV3.kt) implementation.
+A complete sample project is included in this repo under the [ktlint-ruleset-template](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-template) directory. This directory can be cloned, and used as a starting point for a new project containing your custom ruleset.
 
 ## ktlint-ruleset-template
 
-A complete sample project (with tests and build files) is included in this repo under the [ktlint-ruleset-template](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-template) directory (make sure to check [NoVarRuleTest](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt) as it contains some useful information).
+### Gradle build
+
+The [Gradle build file](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/build.gradle.kts) of the sample project includes the setup for:
+
+* publishing the custom ruleset artifact to Maven
+* the custom Gradle task 'ktlintCheck' that is using the Ktlint CLI to run the rules provided by the ktlint project, as well as the custom rule(s) from this project on the project itself ([dogfood principle](https://en.wikipedia.org/wiki/Eating_your_own_dog_food)).
+
+### Rule
+
+The Rule contains the logic for linting and formatting the code. For example, see [NoVarRuleTest](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt).
+
+A rule has to implement one or more of hooks below:
+
+* `Rule.beforeFirstNode`
+* `RuleAutocorrectApproveHandler.beforeVisitChildNodes`
+* `RuleAutocorrectApproveHandler.afterVisitChildNodes`
+* `Rule.afterLastNode`
+
+!!! Tip
+See `ktlint-ruleset-standard` for examples of rules that implement the hooks above.
+
+Upon traversal of the Abstract Syntax Tree (AST), the hooks of the Rule are visited as indicated by their names. The [Jetbrains PsiViewer plugin for IntelliJ IDEA](https://plugins.jetbrains.com/plugin/227-psiviewer) is a convenient tool to inspect the AST for any piece of code.
+
+![Image](../assets/images/psi-viewer.png)
+
+### Rule Set Provider
+
+The RuleSetProvider provides new instances of the rule, see [CustomRuleSetProvider](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/kotlin/yourpkgname/CustomRuleSetProvider.kt) for an example.
+
+`ktlint` is relying on the [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to discover all available "RuleSet"s on the classpath. For this, the RuleSetProvider needs to be registered in file `resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3`, see [Registration for Java ServiceLoader](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3).
+
+### Building the project
 
 ```shell title="Building the ktlint-ruleset-template"
 $ cd ktlint-ruleset-template/
 $ ../gradlew build
 ```
 
+### Running Ktlint CLI with the custom ruleset
+
 ```shell title="Provide code sample that violates rule `custom:no-var"
 $ echo 'var v = 0' > test.kt
 ```
@@ -51,8 +83,8 @@
            - standard:no-unit-return, 
            - standard:no-unused-imports, 
            - standard:no-wildcard-imports, 
-           - standard:op-spacing, 
-           - standard:parameter-list-wrapping, 
+           - standard:op-spacing, ¡
+           - standard:pa¡rameter-list-wrapping, 
            - standard:paren-spacing, 
            - standard:range-spacing, 
            - standard:string-template, 
@@ -64,10 +96,4 @@
 ```
 
 !!! tip
-    Multiple custom rule sets can be loaded at the same time.
-
-## Abstract Syntax Tree (AST)
-
-While writing/debugging [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s it's often helpful to inspect the Abstract Syntax Tree (AST) of the code snippet that is to be linted / formatted. The [Jetbrain PsiViewer plugin for IntelliJ IDEA](https://github.com/JetBrains/psiviewer) is a convenient tool to inspect code as shown below:
-
-![Image](../assets/images/psi-viewer.png)
+Multiple custom rule sets can be loaded at the same time.
diff --git a/documentation/snapshot/docs/api/custom-rule-set.md b/documentation/snapshot/docs/api/custom-rule-set.md
index 83156fa..df02583 100644
--- a/documentation/snapshot/docs/api/custom-rule-set.md
+++ b/documentation/snapshot/docs/api/custom-rule-set.md
@@ -1,17 +1,49 @@
-!!! Tip
-    See [Writing your first ktlint rule](https://medium.com/@vanniktech/writing-your-first-ktlint-rule-5a1707f4ca5b) by [Niklas Baudy](https://github.com/vanniktech).
+You can provide custom rules via a separate ruleset to Ktlint. A ruleset is a JAR containing one or more [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s. 
 
-In a nutshell: a "rule set" is a JAR containing one or more [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s. `ktlint` is relying on the [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to discover all available "RuleSet"s on the classpath. As a ruleset author, all you need to do is to include a `META-INF/services/RuleSetProviderV3` file containing a fully qualified name of your [RuleSetProviderV3](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-ruleset-core/src/main/kotlin/com/pinterest/ktlint/cli/ruleset/core/api/RuleSetProviderV3.kt) implementation.
+ A complete sample project is included in this repo under the [ktlint-ruleset-template](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-template) directory. This directory can be cloned, and used as a starting point for a new project containing your custom ruleset.
 
 ## ktlint-ruleset-template
 
-A complete sample project (with tests and build files) is included in this repo under the [ktlint-ruleset-template](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-template) directory (make sure to check [NoVarRuleTest](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt) as it contains some useful information).
+### Gradle build
+
+The [Gradle build file](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/build.gradle.kts) of the sample project includes the setup for:
+
+* publishing the custom ruleset artifact to Maven
+* the custom Gradle task 'ktlintCheck' that is using the Ktlint CLI to run the rules provided by the ktlint project, as well as the custom rule(s) from this project on the project itself ([dogfood principle](https://en.wikipedia.org/wiki/Eating_your_own_dog_food)).
+
+### Rule
+
+The Rule contains the logic for linting and formatting the code. For example, see [NoVarRuleTest](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt). 
+
+A rule has to implement one or more of hooks below:
+
+* `Rule.beforeFirstNode`
+* `RuleAutocorrectApproveHandler.beforeVisitChildNodes`
+* `RuleAutocorrectApproveHandler.afterVisitChildNodes`
+* `Rule.afterLastNode`
+
+!!! Tip
+    See `ktlint-ruleset-standard` for examples of rules that implement the hooks above.
+
+Upon traversal of the Abstract Syntax Tree (AST), the hooks of the Rule are visited as indicated by their names. The [Jetbrains PsiViewer plugin for IntelliJ IDEA](https://plugins.jetbrains.com/plugin/227-psiviewer) is a convenient tool to inspect the AST for any piece of code.
+
+![Image](../assets/images/psi-viewer.png)
+
+### Rule Set Provider
+
+The RuleSetProvider provides new instances of the rule, see [CustomRuleSetProvider](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/kotlin/yourpkgname/CustomRuleSetProvider.kt) for an example.
+
+`ktlint` is relying on the [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to discover all available "RuleSet"s on the classpath. For this, the RuleSetProvider needs to be registered in file `resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3`, see [Registration for Java ServiceLoader](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3).
+
+### Building the project
 
 ```shell title="Building the ktlint-ruleset-template"
 $ cd ktlint-ruleset-template/
 $ ../gradlew build
 ```
 
+### Running Ktlint CLI with the custom ruleset
+
 ```shell title="Provide code sample that violates rule `custom:no-var"
 $ echo 'var v = 0' > test.kt
 ```
@@ -51,8 +83,8 @@
            - standard:no-unit-return, 
            - standard:no-unused-imports, 
            - standard:no-wildcard-imports, 
-           - standard:op-spacing, 
-           - standard:parameter-list-wrapping, 
+           - standard:op-spacing, ¡
+           - standard:pa¡rameter-list-wrapping, 
            - standard:paren-spacing, 
            - standard:range-spacing, 
            - standard:string-template, 
@@ -65,9 +97,3 @@
 
 !!! tip
     Multiple custom rule sets can be loaded at the same time.
-
-## Abstract Syntax Tree (AST)
-
-While writing/debugging [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s it's often helpful to inspect the Abstract Syntax Tree (AST) of the code snippet that is to be linted / formatted. The [Jetbrain PsiViewer plugin for IntelliJ IDEA](https://github.com/JetBrains/psiviewer) is a convenient tool to inspect code as shown below:
-
-![Image](../assets/images/psi-viewer.png)
diff --git a/ktlint-ruleset-template/build.gradle.kts b/ktlint-ruleset-template/build.gradle.kts
index 70e2033..3a90c6c 100644
--- a/ktlint-ruleset-template/build.gradle.kts
+++ b/ktlint-ruleset-template/build.gradle.kts
@@ -1,58 +1,76 @@
+// This module serves as a sample project for development of a custom ruleset. To avoid any confusion, this build setup is not reusing the
+// build logic of other internal ktlint modules (https://github.com/pinterest/ktlint/issues/3048)..
+
 plugins {
-    id("ktlint-kotlin-common")
-    `java-library`
+    kotlin("jvm") version "2.2.10"
+    // Remove the line below when this custom ruleset is not to be published to maven. If you do want to publish your ruleset to Maven, you
+    // still might need to configure the Maven Central repository in file `settings.gradle.xml` which is not included in the sample project
+    // as it conflicts with the build of the Ktlint itself. Suggested content of that file:
+    //     dependencyResolutionManagement {
+    //          repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    //          repositories {
+    //               mavenCentral()
+    //          }
+    //     }
     `maven-publish`
 }
 
-group = "com.github.username"
+// Change the group name to your liking
+group = "com.github.username.ktlint.ruleset"
+version = "1.0-SNAPSHOT"
 
+// repositories {
+//    mavenCentral()
+// }
+
+// Remove when the Gradle task 'ktlintCheck' is not to be added to the project
+val ktlint: Configuration by configurations.creating
+
+// Update the version numbers of dependencies below to the most recent stable versions
+dependencies {
+    // Remove when the Gradle task 'ktlintCheck' is not to be added to the project
+    ktlint("com.pinterest.ktlint:ktlint-cli:1.7.1")
+
+    implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.7.1")
+    implementation("com.pinterest.ktlint:ktlint-rule-engine-core:1.7.1")
+
+    testImplementation("org.junit.jupiter:junit-jupiter:5.13.4")
+    // Since Gradle 8 the platform launcher needs explicitly be defined as runtime dependency to avoid classpath problems
+    // https://docs.gradle.org/8.12/userguide/upgrading_version_8.html#test_framework_implementation_dependencies
+    testImplementation("org.junit.platform:junit-platform-launcher:1.13.4")
+    testImplementation("org.slf4j:slf4j-simple:2.0.17")
+    testImplementation("com.pinterest.ktlint:ktlint-test:1.7.1")
+}
+
+tasks.test {
+    useJUnitPlatform()
+}
+
+kotlin {
+    jvmToolchain(21)
+}
+
+// Remove when this custom ruleset is not to be published to maven
 val sourcesJar by tasks.registering(Jar::class) {
     dependsOn(tasks.classes)
     archiveClassifier = "sources"
     from(sourceSets.main.map { it.allSource })
 }
 
+// Remove when this custom ruleset is not to be published to maven
 val javadocJar by tasks.registering(Jar::class) {
     dependsOn(tasks.javadoc)
     archiveClassifier = "javadoc"
     from(tasks.javadoc.map { it.destinationDir!! })
 }
 
+// Remove when this custom ruleset is not to be published to maven
 artifacts {
     archives(sourcesJar)
     archives(javadocJar)
 }
 
-val ktlint: Configuration by configurations.creating
-
-dependencies {
-    ktlint(projects.ktlintCli)
-
-    implementation(projects.ktlintCliRulesetCore)
-    implementation(projects.ktlintRuleEngineCore)
-
-    testImplementation(projects.ktlintTest)
-    testRuntimeOnly(libs.slf4j)
-
-    testImplementation(libs.junit5.jupiter)
-    // Since Gradle 8 the platform launcher needs explicitly be defined as runtime dependency to avoid classpath problems
-    // https://docs.gradle.org/8.12/userguide/upgrading_version_8.html#test_framework_implementation_dependencies
-    testRuntimeOnly(libs.junit5.platform.launcher)
-}
-
-val ktlintCheck by tasks.registering(JavaExec::class) {
-    dependsOn(tasks.classes)
-    group = LifecycleBasePlugin.VERIFICATION_GROUP
-    mainClass = "com.pinterest.ktlint.Main"
-    // Adding compiled classes of this ruleset to the classpath so that ktlint validates the ruleset using its own ruleset
-    classpath(ktlint, sourceSets.main.map { it.output })
-    args("--log-level=debug", "src/**/*.kt")
-}
-
-tasks.check {
-    dependsOn(ktlintCheck)
-}
-
+// Remove when this custom ruleset is not to be published to maven
 publishing {
     publications {
         create<MavenPublication>("mavenJava") {
@@ -68,3 +86,18 @@
         }
     }
 }
+
+// Remove when the Gradle task 'ktlintCheck' is not to be added to the project
+val ktlintCheck by tasks.registering(JavaExec::class) {
+    dependsOn(tasks.classes)
+    group = LifecycleBasePlugin.VERIFICATION_GROUP
+    mainClass = "com.pinterest.ktlint.Main"
+    // Adding compiled classes of this ruleset to the classpath so that ktlint validates the ruleset using its own ruleset
+    classpath(ktlint, sourceSets.main.map { it.output })
+    args("--log-level=debug", "src/**/*.kt")
+}
+
+// Remove when the Gradle task 'ktlintCheck' is not to be added to the project
+tasks.check {
+    dependsOn(ktlintCheck)
+}
diff --git a/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt b/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt
index 593cd4d..4058199 100644
--- a/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt
+++ b/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt
@@ -8,11 +8,6 @@
 
     @Test
     fun `No var rule`() {
-        // whenever KTLINT_DEBUG env variable is set to "ast" or -DktlintDebug=ast is used
-        // com.pinterest.ktlint.test.(lint|format) will print AST (along with other debug info) to the stderr.
-        // this can be extremely helpful while writing and testing rules.
-        // uncomment the line below to take a quick look at it
-        // System.setProperty("ktlintDebug", "ast")
         val code =
             """
             fun fn() {