diff --git a/.announce b/.announce index 26e04aae5e..9f5870eb4f 100755 --- a/.announce +++ b/.announce @@ -1,7 +1,10 @@ #!/bin/bash -e # project.version -if [ "$VERSION" == "" ]; then exit 1; fi +if [ "$VERSION" == "" ]; then + echo "VERSION has not been set" + exit 1; +fi if [ "$1" = "-y" ]; then AUTOACCEPT=true @@ -23,43 +26,91 @@ fi echo "Announcing $PREVIOUS_VERSION -> $VERSION" -COMMIT_MESSAGE="Updated refs to latest ($VERSION) release" +DOCUMENTATION_DIR="documentation" +RELEASE_DOCS_DIR="${DOCUMENTATION_DIR}/release-latest" +SNAPSHOT_DOCS_DIR="${DOCUMENTATION_DIR}/snapshot" -if [ "$(git status --porcelain=v1 docs/install/cli.md docs/install/integrations.md)" != "" ]; then - echo "ERROR: To proceed, cli.md and integrations.md must not contain uncommitted changes" +if [ "$(git status --porcelain=v1 $DOCUMENTATION)" != "" ]; then + echo "ERROR: To proceed, the current branch must not contain uncommitted changes in directory '${DOCUMENTATION_DIR}'" # ask for user confirmation if [[ "$AUTOACCEPT" = false ]]; then - read -p "revert changes? (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; else git checkout docs/install/cli.md docs/install/integrations.md; fi + read -p "revert changes? (y/n)? " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + git checkout ${DOCUMENTATION_DIR} + else + exit 1 + fi else - echo "Reverting changes to cli.md and integrations.md" - git checkout docs/install/cli.md docs/install/integrations.md + echo "Reverting changes in directory '${DOCUMENTATION_DIR}'" + git checkout ${DOCUMENTATION_DIR} fi fi -escape_for_sed() { echo "$1" | sed -e 's/[]\/$*.^|[]/\\&/g'; } +#escape_for_sed() { echo "$1" | sed -e 's/[]\/$*.^|[]/\\&/g'; } -# update Docs - -sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" docs/install/cli.md -sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" docs/install/integrations.md -git --no-pager diff docs/install/cli.md docs/install/integrations.md - -# ask for user confirmation -if [[ "$AUTOACCEPT" = false ]]; then - read -p "commit & push (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi -fi +# Make a separate branch because master branch is protected BRANCH="$VERSION-update-refs" - if [ "$(git show-ref refs/heads/$BRANCH)" != "" ]; then echo "ERROR: Branch $BRANCH already exists." if [[ "$AUTOACCEPT" = false ]]; then - read -p "Delete local branch? (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; else git branch -D $BRANCH; fi + read -p "Delete local branch? (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + else + git branch -D $BRANCH + fi else echo "Deleting local branch $BRANCH" git branch -D $BRANCH fi + # Checkout local master branch so that changes to this script can be tested without pushing the script to the remote branch + git checkout --track master -b $BRANCH +else + git checkout --track origin/master -b $BRANCH fi -# Make a separate branch because master branch is protected -git checkout --track origin/master -b $BRANCH && git commit -m "$COMMIT_MESSAGE" docs/install/cli.md docs/install/integrations.md && git push origin $BRANCH +# update version number in snapshot docs + +echo "Updating version numbers in (snapshot) installation documentation" +# On local machine (OSX) Use "sed -i '' ..." instead of "sed -i -e ..." as the latter creates a new file. +# On Github Action workflow the "sed -i -e ..." is required as otherwise it results in failure +# "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 +git --no-pager diff ${DOCUMENTATION_DIR} + +# ask for user confirmation before committing +if [[ "$AUTOACCEPT" = false ]]; then + read -p "Accept changes (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Replace release documentation with current snapshot documentation. + +echo "Replace release documentation with current snapshot documentation" +# Support removal of files which still exists in release docs but which are no longer present in snapshot docs +rm -rf ${RELEASE_DOCS_DIR}/docs/ +cp -r ${SNAPSHOT_DOCS_DIR}/docs/ ${RELEASE_DOCS_DIR}/docs/ +# Note that directory "${SNAPSHOT_DOCS_DIR}/overrides/" should not replace "${RELEASE_DOCS_DIR}/overrides/" +cp -r ${SNAPSHOT_DOCS_DIR}/mkdocs.yml ${RELEASE_DOCS_DIR} +# Add files which previously did not yet exists in the release docs but were present in the snapshot docs +git add --all +# Display sorted list of files changed but do not show contents as that could be a lot +git status --porcelain=v1 documentation | sort + +# Commit and push changes +if [[ "$AUTOACCEPT" = false ]]; then + read -p "Commit and push changes (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi +git commit -m "Updated refs to latest ($VERSION) release" +git push origin $BRANCH diff --git a/.github/workflows/gradle-pr-build.yml b/.github/workflows/build-pull-request.yml similarity index 91% rename from .github/workflows/gradle-pr-build.yml rename to .github/workflows/build-pull-request.yml index 74d155ef82..594c853b94 100644 --- a/.github/workflows/gradle-pr-build.yml +++ b/.github/workflows/build-pull-request.yml @@ -1,9 +1,9 @@ -name: PR Build +name: Build pull request on: push: - branches: - - master + branches: ['master'] + paths: ['**/*.kt', '**/*.kts', '**/*.properties', '**/*.toml'] pull_request: workflow_dispatch: @@ -38,4 +38,3 @@ jobs: run: ./gradlew build ktlintCheck --no-configuration-cache - name: Build with dev Kotlin version run: ./gradlew -PkotlinDev build ktlintCheck --no-configuration-cache - diff --git a/.github/workflows/gradle-docs-publish.yml b/.github/workflows/gradle-docs-publish.yml deleted file mode 100644 index 04d58f0263..0000000000 --- a/.github/workflows/gradle-docs-publish.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Publish documentation -on: - push: - branches: - - master -jobs: - deploy: - runs-on: ubuntu-latest - if: github.repository == 'pinterest/ktlint' - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - run: pip install mkdocs-material - - run: mkdocs gh-deploy --force diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 3f086da669..9b0767d294 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -1,4 +1,5 @@ name: "Validate Gradle Wrapper" + on: [ push, pull_request ] jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/publish-release-build.yml similarity index 73% rename from .github/workflows/release.yml rename to .github/workflows/publish-release-build.yml index 61a206cc85..7ecfb50de2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/publish-release-build.yml @@ -1,4 +1,4 @@ -name: Publish Release +name: Publish release build on : push : @@ -35,6 +35,20 @@ jobs: if: ${{ success() }} uses : ffurrer2/extract-release-notes@v1 + - name: Get version + id: get_version + if: ${{ success() }} + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + + - name: Create zip for dependency managers + if: ${{ success() }} + run: | + cd ktlint-cli/build/run + # Doing this for Homebrew and https://github.com/sdkman/sdkman-cli/wiki/Well-formed-SDK-archives + mkdir -p ktlint-${{ env.version }}/bin + cp ktlint ktlint-${{ env.version }}/bin + zip -rm ktlint-${{ env.version }}.zip ktlint-${{ env.version }} + - name : Create release id: github_release if: ${{ success() }} @@ -48,11 +62,6 @@ jobs: env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} - - name: Get version - id: get_version - if: ${{ success() }} - run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - - name: Bump Homebrew Formula if: ${{ success() }} uses: mislav/bump-homebrew-formula-action@v2 @@ -60,9 +69,17 @@ jobs: COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} with: formula-name: ktlint - download-url: https://github.com/pinterest/ktlint/releases/download/${{ env.version }}/ktlint + download-url: https://github.com/pinterest/ktlint/releases/download/${{ env.version }}/ktlint-${{ env.version }}.zip + + - name: Release to sdkman + if: false + env: + SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} + SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} + SDKMAN_VERSION: ${{ env.version }} + run: ./gradlew :ktlint-cli:sdkMajorRelease - - name: Announce Release + - name: Update Release documentation if: ${{ success() }} run: | git config user.email "<>" | @@ -70,4 +87,3 @@ jobs: ./.announce -y env: VERSION: ${{ env.version }} - diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml new file mode 100644 index 0000000000..3f97b19d9f --- /dev/null +++ b/.github/workflows/publish-release-docs.yml @@ -0,0 +1,41 @@ +name: Publish release documentation + +on: + push: + branches: ['master'] + paths: ['documentation/release-latest/**'] + +jobs: + deploy: + runs-on: ubuntu-latest + if: github.repository == 'pinterest/ktlint' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all commits/branches + - uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install mkdocs and mike + run: pip install mkdocs-material mike + + - name: Config git + run: | + git config user.email "ktlint@github.com" + git config user.name "Ktlint Release Workflow" + + - name: Get last released version + run: echo "version=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV + + - name: Deploy release docs + run: | + echo "Deploy release docs to version ${{ env.version }}" + cd documentation/release-latest + # Release docs are versioned so that user can use relevant docs for the ktlint version they use + mike deploy --push --update-aliases ${{ env.version }} latest + + - name: Update default release docs + run: | + cd documentation/release-latest + mike set-default --push latest diff --git a/.github/workflows/gradle-snapshot-build.yml b/.github/workflows/publish-snapshot-build.yml similarity index 90% rename from .github/workflows/gradle-snapshot-build.yml rename to .github/workflows/publish-snapshot-build.yml index c76a70c8f3..59c9add0d3 100644 --- a/.github/workflows/gradle-snapshot-build.yml +++ b/.github/workflows/publish-snapshot-build.yml @@ -1,8 +1,9 @@ -name: Snapshot Publish +name: Publish snapshot build on: push: branches: [ master ] + paths: ['**/*.kt', '**/*.kts', '**/*.properties', '**/*.toml'] env: SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} diff --git a/.github/workflows/publish-snapshot-docs.yml b/.github/workflows/publish-snapshot-docs.yml new file mode 100644 index 0000000000..4c376dd3bf --- /dev/null +++ b/.github/workflows/publish-snapshot-docs.yml @@ -0,0 +1,32 @@ +name: Publish snapshot documentation + +on: + push: + branches: ['master'] + paths: ['documentation/snapshot/**'] + +jobs: + deploy: + runs-on: ubuntu-latest + if: github.repository == 'pinterest/ktlint' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all commits/branches + - uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install mkdocs and mike + run: pip install mkdocs-material mike + + - name: Config git + run: | + git config user.email "ktlint@github.com" + git config user.name "Ktlint Release Workflow" + + - run: | + cd documentation/snapshot + # The dev-snapshot version has no release number as the version will only be tagged when an official release is build + # This also prevents that multiple snapshot versions of the documentation will be published in parallel + mike deploy --push dev-snapshot diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c26c28338..c2d13ebdc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,39 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +### Added +* Add new experimental rule `no-empty-file` for all code styles. Kotlin file may not be empty. + +* Add new experimental rule `no-empty-file` for all code styles. A kotlin (script) file may not be empty ([#1074](https://github.com/pinterest/ktlint/issues/1074)) + +### Removed + +### Fixed + +### Changed + +## [0.49.1] - 2023-05-12 + ### Added ### Removed ### Fixed * Store path of file containing a lint violation relative to the location of the baseline file itself ([#1962](https://github.com/pinterest/ktlint/issues/1962)) +* Print absolute path of file in lint violations when flag "--relative" is not specified in Ktlint CLI ([#1963](https://github.com/pinterest/ktlint/issues/1963)) +* Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) +* Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) +* Allow to 'unset' the `.editorconfig` property `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` when using `ktlint_official` code style `function-signature` ([#1977](https://github.com/pinterest/ktlint/issues/1977)) +* Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) +* Fix indentation of multiline parameter list in function literal `indent` ([#1976](https://github.com/pinterest/ktlint/issues/1976)) +* Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) +* Extract rule `no-single-line-block-comment` from `comment-wrapping` rule. The `no-single-line-block-comment` rule is added as experimental rule to the `ktlint_official` code style, but it can be enabled explicitly for the other code styles as well. ([#1980](https://github.com/pinterest/ktlint/issues/1980)) +* Clean-up unwanted logging dependencies ([#1998](https://github.com/pinterest/ktlint/issues/1998)) +* Fix directory traversal for patterns referring to paths outside of current working directory or any of it child directories ([#2002](https://github.com/pinterest/ktlint/issues/2002)) ### Changed -* Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers + +* Moved class `Baseline` from `ktlint-cli` to `ktlint-cli-reporter-baseline` so that Baseline functionality is reusable for API Consumers. ## [0.49.0] - 2023-04-21 @@ -1862,6 +1886,7 @@ set in `[*{kt,kts}]` section). ## 0.1.0 - 2016-07-27 +[0.49.1]: https://github.com/pinterest/ktlint/compare/0.49.0...0.49.1 [0.49.0]: https://github.com/pinterest/ktlint/compare/0.48.2...0.49.0 [0.48.2]: https://github.com/pinterest/ktlint/compare/0.48.1...0.48.2 [0.48.1]: https://github.com/pinterest/ktlint/compare/0.48.0...0.48.1 diff --git a/documentation/readme.md b/documentation/readme.md new file mode 100644 index 0000000000..ad49a49f6d --- /dev/null +++ b/documentation/readme.md @@ -0,0 +1,73 @@ +# Documentation + +Two versions of the documentation are kept in the 'master' branch: + +* The `snapshot` version of the documentation applies to the SNAPSHOT versions of ktlint. Upon the publication of the next official release of ktlint, the `release-latest` version of the documentation is replaced with the `snapshot` version. See script `.announce` which is executed by the Github workflow `publish-release-build`. +* The `release-latest` version of the documentation applies to the last officially published version of ktlint. Upon the publication of the next official release of ktlint, this version of the documentation is replaced with the `snapshot` version. See script `.announce` which is executed by the Github workflow `publish-release-build`. + +Whenever a fix is to be made to the documentation, it should be determined in which version(s) of the documentation is to be fixed. Documentation fixes which only apply to the `SNAPSHOT` version of ktlint may only be fixed in the `snapshot` version of the documentation. + +Documentation changes related to the latest released version, and which can not wait to be published with the documentation of the next release, need to be fixed in both the `snapshot` and `release-latest` versions. Only fixing it in `release-latest` results in the fix being lost upon publication of the next official ktlint version. Small typo's can be fixed in both, but it is also okay to only fix them in the `snapshot` only. + +IMPORTANT: Fixing the `snapshot` documentation is more important than fixing the `release-latest` documentation! Try to fix the `release-latest` only when the fix is important (e.g. misleading or confusing for users). + +Docs can be viewed on the local machine in one of following ways: +* Run script `serve-docs-locally.sh` which starts a docker container running mkdocs +* Run command `mike serve` which requires that `python`, `pip`, `mike` and `mkdocs` have been installed (one time only) before + +NOTE: Changes to the documentation have to be submitted as PR. When merging to `master` branch, the Github workflows `publish-snapshot-docs` and `publish-release-docs` take care of publishing the docs to Github Pages. + +## Rebuilding Github Pages manually + +Github Pages deploys the documentation from branch `gh-pages`. Each version of the documentation is stored in a separate directory of that branch. Each of those directories contains a html/css version of the generated documentation. Beside the directories for released versions, two special directories exist. The `latest` directory which is based on the `documentation/release-latest` directory on the `master` branch. The `dev-snapshot` directory is based on the `documentation/snapshot` directory on the `master` branch. + +In case the `gh-pages` branch is corrupt, it can be recreated again. In order to execute commands below, `python`, `pip`, `mkdocs` and `mike` need to be installed on the local machine. The commands have to be executed from the designated directory. Note that after each command a Github Actions workflow is started (see https://github.com/pinterest/ktlint/actions). Wait with executing the next command until the Github Action workflow is completed. In between steps, the command `mike list` can be executed to see what versions have been published. + +1) Clear all doc versions from gh-pages + ```shell + # Execute from directory `documentation/release-latest` + mike delete --all --push + ``` + After the Github Actions workflow is completed, the Github Pages will be empty, and displays a 404 error page. + +2) Recreate the documentation for the last released version: + Reset the active branch to the commit with description `Updated refs to latest (xx.yy.zz) release`. + In directory `documentation/release-latest`: + ```shell + # Execute from directory `documentation/release-latest` + mike deploy --push --update-aliases xx.yy.zz latest # Replace "xx.yy.zz" with the version number. Do not remove or alter tag "latest" + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/xx.yy.zz/ but is not yet available on https://pinterest.github.io/ktlint/ until the Github Action workflow for next command is completed: + ```shell + # Execute from directory `documentation/release-latest` + ``` + mike set-default --push latest + +3) Recreate the documentation for older releases: + Reset the active branch to the commit with description `Updated refs to latest (aa.bb.cc) release`. Note that for releases prior to `0.49.0` additional changes are needed, dependent to which tag the branch is reset: + - In case the `documentation` directory does not exist, then execute the command from the root directory of the project as the `mkdocs.yml` is residing in that directory. + - Add file `documentation/release-latest/overrides/main.html` from branch `master` to the directory where the `mkdocs.yml` file resided + - Add lines below to the `mkdocs.yml`: + ```yaml + extra: + version: + provider: mike + + theme: + name: material + custom_dir: overrides + ``` + In directory `documentation/release-latest`: + ```shell + # Execute from directory `documentation/release-latest` or from root directory of project for versions prior to `0.49.0` + mike deploy --push --update-aliases aa.bb.cc # Replace "aa.bb.cc" with the version number. Do not add tag "latest"! + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/aa.bb.cc/ + +4) Recreate the snapshot documentation + In directory `documentation/snapshot`: + ```shell + # Execute from directory `documentation/snapshot` + mike deploy --push --update-aliases dev-snapshot + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/dev-snapshot/. diff --git a/docs/api/badge.md b/documentation/release-latest/docs/api/badge.md similarity index 100% rename from docs/api/badge.md rename to documentation/release-latest/docs/api/badge.md diff --git a/docs/api/custom-integration.md b/documentation/release-latest/docs/api/custom-integration.md similarity index 100% rename from docs/api/custom-integration.md rename to documentation/release-latest/docs/api/custom-integration.md diff --git a/docs/api/custom-reporter.md b/documentation/release-latest/docs/api/custom-reporter.md similarity index 100% rename from docs/api/custom-reporter.md rename to documentation/release-latest/docs/api/custom-reporter.md diff --git a/docs/api/custom-rule-set.md b/documentation/release-latest/docs/api/custom-rule-set.md similarity index 100% rename from docs/api/custom-rule-set.md rename to documentation/release-latest/docs/api/custom-rule-set.md diff --git a/docs/api/index.md b/documentation/release-latest/docs/api/index.md similarity index 100% rename from docs/api/index.md rename to documentation/release-latest/docs/api/index.md diff --git a/docs/api/overview.md b/documentation/release-latest/docs/api/overview.md similarity index 100% rename from docs/api/overview.md rename to documentation/release-latest/docs/api/overview.md diff --git a/docs/assets/images/favicon.ico b/documentation/release-latest/docs/assets/images/favicon.ico similarity index 100% rename from docs/assets/images/favicon.ico rename to documentation/release-latest/docs/assets/images/favicon.ico diff --git a/docs/assets/images/module-dependencies.png b/documentation/release-latest/docs/assets/images/module-dependencies.png similarity index 100% rename from docs/assets/images/module-dependencies.png rename to documentation/release-latest/docs/assets/images/module-dependencies.png diff --git a/docs/assets/images/psi-viewer.png b/documentation/release-latest/docs/assets/images/psi-viewer.png similarity index 100% rename from docs/assets/images/psi-viewer.png rename to documentation/release-latest/docs/assets/images/psi-viewer.png diff --git a/docs/assets/images/rule-dependencies.png b/documentation/release-latest/docs/assets/images/rule-dependencies.png similarity index 100% rename from docs/assets/images/rule-dependencies.png rename to documentation/release-latest/docs/assets/images/rule-dependencies.png diff --git a/docs/contributing/code-of-conduct.md b/documentation/release-latest/docs/contributing/code-of-conduct.md similarity index 100% rename from docs/contributing/code-of-conduct.md rename to documentation/release-latest/docs/contributing/code-of-conduct.md diff --git a/docs/contributing/guidelines.md b/documentation/release-latest/docs/contributing/guidelines.md similarity index 100% rename from docs/contributing/guidelines.md rename to documentation/release-latest/docs/contributing/guidelines.md diff --git a/docs/contributing/index.md b/documentation/release-latest/docs/contributing/index.md similarity index 100% rename from docs/contributing/index.md rename to documentation/release-latest/docs/contributing/index.md diff --git a/docs/contributing/overview.md b/documentation/release-latest/docs/contributing/overview.md similarity index 100% rename from docs/contributing/overview.md rename to documentation/release-latest/docs/contributing/overview.md diff --git a/docs/faq.md b/documentation/release-latest/docs/faq.md similarity index 100% rename from docs/faq.md rename to documentation/release-latest/docs/faq.md diff --git a/docs/index.md b/documentation/release-latest/docs/index.md similarity index 100% rename from docs/index.md rename to documentation/release-latest/docs/index.md diff --git a/docs/install/cli.md b/documentation/release-latest/docs/install/cli.md similarity index 98% rename from docs/install/cli.md rename to documentation/release-latest/docs/install/cli.md index 7a6471aa29..69d8457e78 100644 --- a/docs/install/cli.md +++ b/documentation/release-latest/docs/install/cli.md @@ -12,7 +12,7 @@ All releases of `ktlint` can be downloaded from the [releases](https://github.co A particular version of `ktlint` can be downloaded with next command which also changes the file to an executable in directory `/usr/local/bin`: ```sh title="Download" -curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ +curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ ``` !!! tip "Curl not installed or behind proxy" @@ -48,6 +48,11 @@ Install with [MacPorts](https://www.macports.org/) port install ktlint ``` +Install with [SDKMAN! on macOS and Linux](https://sdkman.io/) +```sh +sdk install ktlint +``` + On Arch Linux install package [ktlint AUR](https://aur.archlinux.org/packages/ktlint/). ## Command line usage diff --git a/docs/install/index.md b/documentation/release-latest/docs/install/index.md similarity index 100% rename from docs/install/index.md rename to documentation/release-latest/docs/install/index.md diff --git a/docs/install/integrations.md b/documentation/release-latest/docs/install/integrations.md similarity index 98% rename from docs/install/integrations.md rename to documentation/release-latest/docs/install/integrations.md index 76f21b8e2f..854077e597 100644 --- a/docs/install/integrations.md +++ b/documentation/release-latest/docs/install/integrations.md @@ -56,7 +56,7 @@ See [cli usage](../cli) for arguments that can be supplied to `ktlint`. com.pinterest ktlint - 0.49.0 + 0.49.1 @@ -117,7 +117,7 @@ configurations { } dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL)) } @@ -167,7 +167,7 @@ The configuration below, defines following task: val ktlint by configurations.creating dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) } diff --git a/docs/install/overview.md b/documentation/release-latest/docs/install/overview.md similarity index 100% rename from docs/install/overview.md rename to documentation/release-latest/docs/install/overview.md diff --git a/docs/install/snapshot-build.md b/documentation/release-latest/docs/install/snapshot-build.md similarity index 100% rename from docs/install/snapshot-build.md rename to documentation/release-latest/docs/install/snapshot-build.md diff --git a/docs/readme.md b/documentation/release-latest/docs/readme.md similarity index 100% rename from docs/readme.md rename to documentation/release-latest/docs/readme.md diff --git a/docs/rules/code-styles.md b/documentation/release-latest/docs/rules/code-styles.md similarity index 100% rename from docs/rules/code-styles.md rename to documentation/release-latest/docs/rules/code-styles.md diff --git a/docs/rules/configuration-intellij-idea.md b/documentation/release-latest/docs/rules/configuration-intellij-idea.md similarity index 100% rename from docs/rules/configuration-intellij-idea.md rename to documentation/release-latest/docs/rules/configuration-intellij-idea.md diff --git a/docs/rules/configuration-ktlint.md b/documentation/release-latest/docs/rules/configuration-ktlint.md similarity index 95% rename from docs/rules/configuration-ktlint.md rename to documentation/release-latest/docs/rules/configuration-ktlint.md index 891d08843a..bd7533867f 100644 --- a/docs/rules/configuration-ktlint.md +++ b/documentation/release-latest/docs/rules/configuration-ktlint.md @@ -59,11 +59,14 @@ This setting only takes effect when rule `final-newline` is enabled. ## Force multiline function signature based on number of parameters -By default, the number of parameters in a function signature is not relevant when rewriting the function signature. Only the maximum line length determines when a function signature should be written on a single line or with multiple lines. Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` can be used, to force a multiline function signature in case the function contain at least a number of parameters even in case the function signature would fit on a single line. Use value `-1` (default) to disable this setting. +Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` forces a multiline function signature in case the function contains the specified minimum number of parameters even in case the function signature would fit on a single line. Use value `unset` (default) to disable this setting. + +!!! note + By default, the `ktlint_official` code style wraps parameters of functions having at least 2 parameters. For other code styles, this setting is disabled by default. ```ini [*.{kt,kts}] -ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than= -1 +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than=unset ``` This setting only takes effect when rule `function-signature` is enabled. diff --git a/docs/rules/dependencies.md b/documentation/release-latest/docs/rules/dependencies.md similarity index 100% rename from docs/rules/dependencies.md rename to documentation/release-latest/docs/rules/dependencies.md diff --git a/docs/rules/experimental.md b/documentation/release-latest/docs/rules/experimental.md similarity index 97% rename from docs/rules/experimental.md rename to documentation/release-latest/docs/rules/experimental.md index 7aa9f1dd3a..61561d524b 100644 --- a/docs/rules/experimental.md +++ b/documentation/release-latest/docs/rules/experimental.md @@ -345,6 +345,32 @@ This rule can also be suppressed with the IntelliJ IDEA inspection suppression ` Rule id: `property-naming` +## No single line block comments + +A single line block comment should be replaced with an EOL comment when possible. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * Some comment + */ + val foo = "foo" // Some comment + val foo = { /* no-op */ } + + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment */ + val foo = "foo" /* Some comment */ + ``` + +Rule id: `no-single-line-block-comment` + ## Spacing ### No blank lines in list diff --git a/docs/rules/index.md b/documentation/release-latest/docs/rules/index.md similarity index 100% rename from docs/rules/index.md rename to documentation/release-latest/docs/rules/index.md diff --git a/docs/rules/standard.md b/documentation/release-latest/docs/rules/standard.md similarity index 98% rename from docs/rules/standard.md rename to documentation/release-latest/docs/rules/standard.md index 12ff505919..3bf94be6e5 100644 --- a/docs/rules/standard.md +++ b/documentation/release-latest/docs/rules/standard.md @@ -247,13 +247,13 @@ Indentation formatting - respects `.editorconfig` `indent_size` with no continua ``` !!! note - This rule handles indentation for many different language constructs which can not be summarized with a few example. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. + This rule handles indentation for many different language constructs which can not be summarized with a few examples. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. Rule id: `indent` ## Max line length -Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules than that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. +Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules then that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. === "[:material-heart:](#) Ktlint" @@ -261,7 +261,7 @@ Ensures that lines do not exceed the given length of `.editorconfig` property `m // Assume that the last allowed character is // at the X character on the right X // Lines below are accepted although the max - // line length is exceeeded. + // line length is exceeded. package com.toooooooooooooooooooooooooooo.long import com.tooooooooooooooooooooooooooooo.long val foo = @@ -450,7 +450,6 @@ Rule id: `no-empty-class-body` ```kotlin fun bar() { - val a = 2 } ``` @@ -458,6 +457,7 @@ Rule id: `no-empty-class-body` ```kotlin fun bar() { + val a = 2 } ``` @@ -867,7 +867,7 @@ Rule id: `wrapping` ### Comment wrapping -A block comment should start and end on a line that does not contain any other element. A block comment should not be used as end of line comment. +A block comment should start and end on a line that does not contain any other element. === "[:material-heart:](#) Ktlint" diff --git a/mkdocs.yml b/documentation/release-latest/mkdocs.yml similarity index 95% rename from mkdocs.yml rename to documentation/release-latest/mkdocs.yml index 27b0cba4e6..ee9f9843b0 100644 --- a/mkdocs.yml +++ b/documentation/release-latest/mkdocs.yml @@ -1,8 +1,16 @@ site_name: Ktlint site_url: https://pinterest.github.io/ktlint/ +site_dir: build/site +docs_dir: docs + +extra: + version: + provider: mike + theme: name: material + custom_dir: overrides favicon: assets/images/favicon.ico palette: # Palette toggle for light mode diff --git a/documentation/release-latest/overrides/main.html b/documentation/release-latest/overrides/main.html new file mode 100644 index 0000000000..033fa3c1f5 --- /dev/null +++ b/documentation/release-latest/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} +You're not viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/documentation/release-latest/serve-docs-locally.sh b/documentation/release-latest/serve-docs-locally.sh new file mode 100755 index 0000000000..f4f4522d7b --- /dev/null +++ b/documentation/release-latest/serve-docs-locally.sh @@ -0,0 +1,12 @@ +# !/bin/bash + +echo "Serving docs from directory '$(basename "${PWD##*/}")'" +echo "" + +mkdocs serve +if [[ $? -ne 0 ]]; then + echo "Invalid command. Please ensure that 'mkdocs' is installed." + echo " - If needed install python3" + echo " - If needed run 'pip install mkdocs'" + echo "Or run 'docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material'" +fi diff --git a/documentation/snapshot/docs/api/badge.md b/documentation/snapshot/docs/api/badge.md new file mode 100644 index 0000000000..40b87210b4 --- /dev/null +++ b/documentation/snapshot/docs/api/badge.md @@ -0,0 +1,6 @@ +If you want to display a badge to show that your project is linted and formatted using `'ktlint` than you can add the +[![ktlint](https://img.shields.io/badge/ktlint%20code--style-%E2%9D%A4-FF4081)](https://pinterest.github.io/ktlint/) badge: + +```md title="Ktlint code style badge" +[![ktlint](https://img.shields.io/badge/ktlint%20code--style-%E2%9D%A4-FF4081)](https://pinterest.github.io/ktlint/) +``` diff --git a/documentation/snapshot/docs/api/custom-integration.md b/documentation/snapshot/docs/api/custom-integration.md new file mode 100644 index 0000000000..d6ebe68b3c --- /dev/null +++ b/documentation/snapshot/docs/api/custom-integration.md @@ -0,0 +1,116 @@ +# Custom integration + +!!! warning + This page is based on Ktlint `0.49.x` which has to be released. Most concepts are also applicable for `0.48.x`. + +## Ktlint Rule Engine + +The `Ktlint Rule Engine` is the central entry point for custom integrations with the `Ktlint API`. See [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) for a basic example on how to invoke the `Ktlint Rule Engine`. This example also explains how the logging of the `Ktlint Rule Engine` can be configured to your needs. + +The `KtLintRuleEngine` instance only needs to be created once for the entire lifetime of your application. Reusing the same instance results in better performance due to caching. + +```kotlin title="Creating the KtLintRuleEngine" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + ) +``` + +### Rule provider + +The `KtLintRuleEngine` must be configured with at least one `RuleProvider`. A `RuleProvider` is a lambda which upon request of the `KtLintRuleEngine` provides a new instance of a specific rule. You can either provide any of the standard rules provided by KtLint or with your own custom rules, or with a combination of both. +```kotlin title="Creating a set of RuleProviders" +val KTLINT_API_CONSUMER_RULE_PROVIDERS = + setOf( + // Can provide custom rules + RuleProvider { NoVarRule() }, + // but also reuse rules from KtLint rulesets + RuleProvider { IndentationRule() }, + ) +``` + +### Editor config: defaults & overrides + +When linting and formatting files, the `KtlintRuleEngine` takes the `.editorconfig` file(s) into account which are found on the path to the file. A property which is specified in the `editorConfigOverride` property of the `KtLintRuleEngine` takes precedence above the value of that same property in the `.editorconfig` file. The `editorConfigDefaults` property of the `KtLintRuleEngine` can be used to specify the fallback values for properties in case that property is not defined in the `.editorconfig` file (or in the `editorConfigOverride` property). + +```kotlin title="Specifying the editorConfigOverride" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigOverride = EditorConfigOverride.from( + INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE, + INDENT_SIZE_PROPERTY to 4 + ) + ) +``` + +The `editorConfigOverride` property takes an `EditorConfigProperty` as key. KtLint defines several such properties, but they can also be defined as part of a custom rule. + +The `editorConfigDefaults` property is more cumbersome to define as it is based directly on the data format of the `ec4j` library which is used for parsing the `.editorconfig` file. + +The defaults can be loaded from a path or a directory. If a path to a file is specified, the name of the file does not necessarily have to end with `.editorconfig`. If a path to a directory is specified, the directory should contain a file with name `.editorconfig`. Note that the `propertyTypes` have to be derived from the same collection of rule providers that are specified in the `ruleProviders` property of the `KtLintRuleEngine`. + +```kotlin title="Specifying the editorConfigDefaults using an '.editorconfig' file" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigDefaults = EditorConfigDefaults.load( + path = Paths.get("/some/path/to/editorconfig/file/or/directory"), + propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(), + ) + ) +``` +If you want to include all RuleProviders of the Ktlint project than you can easily retrieve the collection using `StandardRuleSetProvider().getRuleProviders()`. + +The `EditorConfigDefaults` property can also be specified programmatically as is shown below: + +```kotlin title="Specifying the editorConfigDefaults programmatically" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigDefaults = EditorConfigDefaults( + org.ec4j.core.model.EditorConfig + .builder() + // .. add relevant properties + .build() + ) + ) +``` + +### Lint & format + +Once the `KtLintRuleEngine` has been defined, it is ready to be invoked for each file or code snippet that has to be linted or formatted. The the `lint` and `format` functions take a `Code` instance as parameter. Such an instance can either be created from a file +```kotlin title="Code from file" +val code = Code.fromFile( + File("/some/path/to/file") +) +``` +or a code snippet (set `script` to `true` to handle the snippet as Kotlin script): +```kotlin title="Code from snippet" +val code = Code.fromSnippet( + """ + val code = "some-code" + """.trimIndent() +) +``` + +The `lint` function is invoked with a lambda which is called each time a `LintError` is found and does not return a result. +```kotlin title="Specifying the editorConfigDefaults programmatically" +ktLintRuleEngine + .lint(codeFile) { lintError -> + // handle + } +``` + +The `format` function is invoked with a lambda which is called each time a `LintError` is found and returns the formatted code as result. Note that the `LintError` should be inspected for errors that could not be autocorrected. +```kotlin title="Specifying the editorConfigDefaults programmatically" +val formattedCode = + ktLintRuleEngine + .format(codeFile) { lintError -> + // handle + } +``` + +## Logging + +Ktlint uses the `io.github.microutils:kotlin-logging` which is a `slf4j` wrapper. As API consumer you can choose which logging framework you want to use and configure that framework to your exact needs. The [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) contains an example with `org.slf4j:slf4j-simple` as logging provider and a customized configuration which shows logging at `DEBUG` level for all classes except one specific class which only displays logging at `WARN` level. diff --git a/documentation/snapshot/docs/api/custom-reporter.md b/documentation/snapshot/docs/api/custom-reporter.md new file mode 100644 index 0000000000..843c558960 --- /dev/null +++ b/documentation/snapshot/docs/api/custom-reporter.md @@ -0,0 +1,18 @@ +## Build a custom reporter +Take a look at [ktlint-cli-reporter-plain](https://github.com/pinterest/ktlint/tree/master/ktlint-cli-reporter-plain). + +In short, all you need to do is to implement a +[ReporterV2](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterV2.kt) and make it available by registering +a custom [ReporterProviderV2](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt) using +`META-INF/services/com.pinterest.ktlint.cli.reporter.core.api.ReporterProviderV2`. Pack all of that into a JAR and you're done. + +To load a custom (3rd party) reporter use `ktlint --reporter=name,artifact=/path/to/custom-ktlint-reporter.jar` +(see `ktlint --help` for more). + +## Third party reporters + +Known third-party reporters: + +* [kryanod/ktlint-junit-reporter](https://github.com/kryanod/ktlint-junit-reporter) reports ktlint output as an xml file in JUnit format so that the ktlint report can be made visible on the Merge Request page. +* [musichin/ktlint-github-reporter](https://github.com/musichin/ktlint-github-reporter) uses [GitHub workflow commands](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) to set error messages for `ktlint` issues. +* [tobi2k/ktlint-gitlab-reporter](https://github.com/tobi2k/ktlint-gitlab-reporter) provides output in JSON format that can be parsed by GitLab automatically. diff --git a/documentation/snapshot/docs/api/custom-rule-set.md b/documentation/snapshot/docs/api/custom-rule-set.md new file mode 100644 index 0000000000..3366e3c248 --- /dev/null +++ b/documentation/snapshot/docs/api/custom-rule-set.md @@ -0,0 +1,73 @@ +!!! Tip + See [Writing your first ktlint rule](https://medium.com/@vanniktech/writing-your-first-ktlint-rule-5a1707f4ca5b) by [Niklas Baudy](https://github.com/vanniktech). + +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-core/src/main/kotlin/com/pinterest/ktlint/ruleset/core/api/RuleSetProviderV3.kt) implementation. + +## 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). + +```shell title="Building the ktlint-ruleset-template" +$ cd ktlint-ruleset-template/ +$ ../gradlew build +``` + +```shell title="Provide code sample that violates rule `custom:no-var" +$ echo 'var v = 0' > test.kt +``` + +```shell title="Running the ktlint-ruleset-template" hl_lines="1 40 43" +$ ktlint -R build/libs/ktlint-ruleset-template.jar --log-level=debug --relative test.kt + +18:13:21.026 [main] DEBUG com.pinterest.ktlint.internal.RuleSetsLoader - JAR ruleset provided with path "/../ktlint/ktlint-ruleset-template/build/libs/ktlint-ruleset-template.jar" +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "baseline" id. +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "checkstyle" id. +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "json" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "html" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "plain" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "sarif" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Initializing "plain" reporter with {verbose=false, color=false, color_name=DARK_GRAY} +[DEBUG] Rule with id 'standard:max-line-length' should run after the rule with id 'trailing-comma'. However, the latter rule is not loaded and is allowed to be ignored. For best results, it is advised load the rule. +[DEBUG] Rules will be executed in order below (unless disabled): + - standard:filename, + - standard:final-newline, + - standard:chain-wrapping, + - standard:colon-spacing, + - standard:comma-spacing, + - standard:comment-spacing, + - standard:curly-spacing, + - standard:dot-spacing, + - standard:import-ordering, + - standard:keyword-spacing, + - standard:modifier-order, + - standard:no-blank-line-before-rbrace, + - standard:no-consecutive-blank-lines, + - standard:no-empty-class-body, + - standard:no-line-break-after-else, + - standard:no-line-break-before-assignment, + - standard:no-multi-spaces, + - standard:no-semi, + - standard:no-trailing-spaces, + - standard:no-unit-return, + - standard:no-unused-imports, + - standard:no-wildcard-imports, + - standard:op-spacing, + - standard:parameter-list-wrapping, + - standard:paren-spacing, + - standard:range-spacing, + - standard:string-template, + - custom:no-var, + - standard:indent, + - standard:max-line-length +`text test.kt:1:1: Unexpected var, use val instead (cannot be auto-corrected)` +18:13:21.893 [main] DEBUG com.pinterest.ktlint.Main - 872ms / 1 file(s) / 1 error(s) +``` + +!!! 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/ruleset/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/documentation/snapshot/docs/api/index.md b/documentation/snapshot/docs/api/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/snapshot/docs/api/overview.md b/documentation/snapshot/docs/api/overview.md new file mode 100644 index 0000000000..333d94a497 --- /dev/null +++ b/documentation/snapshot/docs/api/overview.md @@ -0,0 +1,16 @@ +Ktlint has an open API with which you can integrate. + +The diagram below show the internal module structure of KtLint. + +![Image](../../assets/images/module-dependencies.png) + +The `Ktlint Rule Engine` is by far the most important module. It is responsible for executing the linting and formatting of the source code. The Rule Engine itself does not contain any rules. Rules are provided by API Consumers. + +The `Ktlint CLI` is an API Consumer of the `Ktlint Rule Engine`. Together with the `Ktlint Ruleset Standard` and the `Ktlint Reporter` modules the CLI offers a standalone tool which can easily be run from the commandline. Also, the `Ktlint CLI` can easily be used with custom rulesets and/or reporters. + +The `Ktlint Ruleset Core` module contains the logic which is required by each API Consumer of the `Ktlint Rule Engine`, the `Ktlint Ruleset Standard` and custom rulesets. + +The module `Ktlint Test` provide functionalities like `assertThatRule` which is used to write unit tests in a fluent AssertJ look-a-like style and can also be used for testing of custom rules. + +The `Ktlint logger` module provides functionality for writing log messages. + diff --git a/documentation/snapshot/docs/assets/images/favicon.ico b/documentation/snapshot/docs/assets/images/favicon.ico new file mode 100644 index 0000000000..c7ff61748f Binary files /dev/null and b/documentation/snapshot/docs/assets/images/favicon.ico differ diff --git a/documentation/snapshot/docs/assets/images/module-dependencies.png b/documentation/snapshot/docs/assets/images/module-dependencies.png new file mode 100644 index 0000000000..029bfbe21f Binary files /dev/null and b/documentation/snapshot/docs/assets/images/module-dependencies.png differ diff --git a/documentation/snapshot/docs/assets/images/psi-viewer.png b/documentation/snapshot/docs/assets/images/psi-viewer.png new file mode 100644 index 0000000000..88ce1d224c Binary files /dev/null and b/documentation/snapshot/docs/assets/images/psi-viewer.png differ diff --git a/documentation/snapshot/docs/assets/images/rule-dependencies.png b/documentation/snapshot/docs/assets/images/rule-dependencies.png new file mode 100644 index 0000000000..f13326a160 Binary files /dev/null and b/documentation/snapshot/docs/assets/images/rule-dependencies.png differ diff --git a/documentation/snapshot/docs/contributing/code-of-conduct.md b/documentation/snapshot/docs/contributing/code-of-conduct.md new file mode 100644 index 0000000000..84960873d9 --- /dev/null +++ b/documentation/snapshot/docs/contributing/code-of-conduct.md @@ -0,0 +1,27 @@ +At Pinterest, we work hard to ensure that our work environment is welcoming and inclusive to as many people as possible. We are committed to creating this environment for everyone involved in our open source projects as well. We welcome all participants regardless of ability, age, ethnicity, identified gender, religion (or lack there of), sexual orientation and socioeconomic status. + +This code of conduct details our expectations for upholding these values. + +## Good behavior + +We expect members of our community to exhibit good behavior including (but of course not limited to): + +- Using intentional and empathetic language. +- Focusing on resolving instead of escalating conflict. +- Providing constructive feedback. + +## Unacceptable behavior + +Some examples of unacceptable behavior (again, this is not an exhaustive list): + +- Harassment, publicly or in private. +- Trolling. +- Sexual advances (this isn’t the place for it). +- Publishing other’s personal information. +- Any behavior which would be deemed unacceptable in a professional environment. + +## Recourse + +If you are witness to or the target of unacceptable behavior, it should be reported to Pinterest at opensource-policy@pinterest.com. All reporters will be kept confidential and an appropriate response for each incident will be evaluated. + +If the maintainers do not uphold and enforce this code of conduct in good faith, community leadership will hold them accountable. diff --git a/documentation/snapshot/docs/contributing/guidelines.md b/documentation/snapshot/docs/contributing/guidelines.md new file mode 100644 index 0000000000..f7fcd7acf7 --- /dev/null +++ b/documentation/snapshot/docs/contributing/guidelines.md @@ -0,0 +1,59 @@ +First off, thanks for taking the time to contribute! This guide will answer some common questions about how this project works. + +While this is a Pinterest open source project, we welcome contributions from everyone. Regular outside contributors can become project maintainers. + +## Help + +If you're having trouble using this project, please start by reading all documentation and searching for solutions in the existing open and closed issues. + +## Security + +If you've found a security issue in one of our open source projects, please report it at [Bugcrowd](https://bugcrowd.com/pinterest); you may even make some money! + +## Code of Conduct + +Please be sure to read and understand our [code of conduct](../code-of-conduct/). We work hard to ensure that our projects are welcoming and inclusive to as many people as possible. + +## Reporting Issues + +If you have a bug report, please provide as much information as possible so that we can help you out: + +- Version of the project you're using. +- Code (or even better a sample project) which reproduce the issue. +- Steps which reproduce the issue. +- Stack traces for crashes. +- Any logs produced. + +## Making Changes + +!!! tip + `ktlint` only provides rules that enforce the [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) or [Android Kotlin style guide](https://developer.android.com/kotlin/style-guide). If your change is more opinionated than please [file an issue](https://github.com/pinterest/ktlint/issues/new) first so that it can be discussed amongst the community. Rules which are too opinionated might be better published as a custom rule set. + +1. Fork this repository to your own account +2. Make your changes and verify that tests pass +3. Commit your work and push to a new branch on your fork +4. Submit a pull request +5. Participate in the code review process by responding to feedback + +Once there is agreement that the code is in good shape, one of the project's maintainers will merge your contribution. + +To increase the chances that your pull request will be accepted: + +- Follow the coding style +- Write tests for your changes +- Write a good commit message +- Provide context in the pull request description. + +New rules have to implement the `Rule.Experimental` interface so that the rule will only be run for user who have opted in to use experimental rules. Once the rule is stable, the marker interface `Rule.Experimental` can be removed. + +## Updating dependencies + +This project has enabled [Gradle dependencies verification](https://docs.gradle.org/6.2/userguide/dependency_verification.html). On adding/updating any dependency, ensure that you've added dependency provided checksum/signature to `gradle/verification-metadata.xml` file. + +## Using kotlin development versions + +Add following flag - `-PkotlinDev` to enable kotlin development version. + +## License + +By contributing to this project, you agree that your contributions will be licensed under its [license](/#legal). diff --git a/documentation/snapshot/docs/contributing/index.md b/documentation/snapshot/docs/contributing/index.md new file mode 100644 index 0000000000..e5c323966a --- /dev/null +++ b/documentation/snapshot/docs/contributing/index.md @@ -0,0 +1 @@ +## Contributing guidelines diff --git a/documentation/snapshot/docs/contributing/overview.md b/documentation/snapshot/docs/contributing/overview.md new file mode 100644 index 0000000000..cb4aca74de --- /dev/null +++ b/documentation/snapshot/docs/contributing/overview.md @@ -0,0 +1,19 @@ +!!! important + Make sure to read the [Contributing guideline](guidelines.md) and the [code of conduct](code-of-conduct.md) first. + +## Development + +Development starts with cloning and building the project on your local machine: + +```sh +git clone https://github.com/pinterest/ktlint && cd ktlint +./gradlew tasks # shows how to build, test, run, etc. project +``` + +!!! tip + To open and run `ktlint` in Intellij IDEA: + + * File -> Open.... + * You'll also need to set the "Project language level" to 8 in "Project Settings" (File -> Project Structure... -> Project). + * To run `ktlint` - right-click on `ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt` -> Run. + diff --git a/documentation/snapshot/docs/faq.md b/documentation/snapshot/docs/faq.md new file mode 100644 index 0000000000..9f47b60e13 --- /dev/null +++ b/documentation/snapshot/docs/faq.md @@ -0,0 +1,186 @@ +## Why should I use ktlint? + +the short answer is **Simplicity**. + +Spending time on configuration (and maintenance down the road) of hundred-line long style config file(s) is counter-productive. Instead of wasting your energy on something that has no business value - focus on what really matters (not debating whether to use tabs or spaces). + +By using ktlint you put the importance of code clarity and community conventions over personal preferences. This makes things easier for people reading your code as well as frees you from having to document and explain what style potential contributor(s) have to follow. + +ktlint is a single binary with both linter & formatter included. All you need is to drop it in (no need to get [overwhelmed](https://en.wikipedia.org/wiki/Decision_fatigue) while choosing among [dozens of code style options](https://checkstyle.sourceforge.net/checks.html)). + +## How do I enable or disable a rule? + +An individual rule can be enabled or disabled with a rule property. The name of the rule property consists of the `ktlint_` prefix followed by the rule set id followed by a `_` and the rule id. Examples: +```editorconfig +ktlint_standard_final-newline = disabled # Disables the `final-newline` rule in the `standard` rule set provided by KtLint +ktlint_standard_some-experimental-rule = enabled # Enables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + +## How do I enable or disable a rule set? + +All rules in a rule set can be enabled or disabled with a rule set property. The name of the rule set property consists of the `ktlint_` prefix followed by the rule set id. Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + All rules from the `standard` and custom rule sets are *enabled* by default and can optionally be disabled in the `.editorconfig`. All rules from the `experimental` rule set are *disabled* by default and can optionally be enabled in the `.editorconfig`. + +## Can I have my own rules on top of ktlint? + +Absolutely, "no configuration" doesn't mean "no extensibility". You can add your own ruleset(s) to discover potential bugs, check for anti-patterns, etc. + +See [adding a custom rule set](../api/custom-rule-set/) for more information. + +## How do I suppress errors for a line/block/file? + +!!! tip + Suppressing a `ktlint` violation is meant primarily as an escape latch for the rare cases when **ktlint** is not able to produce the correct result. Please report any such instances using [GitHub Issues](https://github.com/pinterest/ktlint/issues)). + +To disable a specific rule you'll need the rule identifier which is displayed at the end of the lint error. + +An error can be suppressed using: + +* EOL comments +* Block comments +* @Suppress annotations + +=== "[:material-heart:](#) Suppress annotation" + + ```kotlin + // Suppressing all rules for the entire file + @file:Suppress("ktlint") + + // Suppress a single rule for the annotated construct + @Suppress("ktlint:standard:no-wildcard-imports") + import foo.* + + // Suppress multiple rules for the annotated construct + @Suppress("ktlint:standard:no-wildcard-imports", "ktlint:standard:other-rule-id") + import foo.* + + // Suppress all rules for the annotated construct + @Suppress("ktlint") + import foo.* + ``` +=== "[:material-heart:](#) EOL comments" + + ```kotlin + // Suppress a single rule for the commented line + import foo.* // ktlint-disable standard_no-wildcard-imports + + // Suppress multiple rules for the commented line + import foo.* // ktlint-disable standard_no-wildcard-imports standard_other-rule-id + + // Suppress all rules for the commented line + import foo.* // ktlint-disable + ``` + +=== "[:material-heart-off-outline:](#) Block comments" + + ```kotlin + // Suppress a single rule for all code between the start and end tag + /* ktlint-disable standard_no-wildcard-imports */ + import foo.* + /* ktlint-disable standard_no-wildcard-imports */ + + // Suppress multiple rules for all code between the start and end tag + /* ktlint-disable standard_no-wildcard-imports standard_no-wildcard-imports */ + import foo.* + /* ktlint-enable standard_no-wildcard-imports standard_no-wildcard-imports */ + + // Suppress all rules for all code between the start and end tag + /* ktlint-disable */ + import foo.* + /* ktlint-enable */ + ``` + +!!! important + When using the block comments, the `ktlint-enable` directive needs to specify the exact same rule-id's and in the same order as the `ktlint-disable` directive. + +!!! warning + From a consistency perspective seen, it might be best to **not** mix the (EOL/Block) comment style with the annotation style in the same project. + +## How do I globally disable a rule without `.editorconfig`? + +When using Ktlint CLI, you may pass a list of disabled rules via the `--disabled_rules` command line flag. The value is a comma separated list of rule id's that have to be disabled. The rule id must be fully qualified (e.g. must be prefixed with the rule set id). + + +## Why is `.editorconfig` property `disabled_rules` deprecated and how do I resolve this? + +The `.editorconfig` properties `disabled_rules` and `ktlint_disabled_rules` are deprecated as of KtLint version `0.48` and are removed in version `0.49`. Those properties contain a comma separated list of rules which are disabled. Using a comma separated list of values has some disadvantages. + +A big disadvantage is that it is not possible to override the property partially in an `.editorconfig` file in a subpackage. Another disadvantage is that it is not possible to express explicitly that a rule is enabled. Lastly, (qualified) rule ids can be 20 characters or longer, which makes a list with multiple entries hard to read. + +Starting with KtLint `0.48` entire rule sets and individual rules can be disabled / enabled with a separate property per rule (set). Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_standard_final-newline = enabled # Enables the `final-newline` rule in the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +ktlint_standard_some-experimental-rule = disabled # Disables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + All rules from the `standard` and custom rule sets are *enabled* by default and can optionally be disabled in the `.editorconfig`. All rules from the `experimental` rule set are *disabled* by default and can optionally be enabled in the `.editorconfig`. + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + +## Why is wildcard import `java.util.*` not reported by the `no-wildcard-imports` rule? + +The `no-wildcard-imports` rule forbids wildcard imports, except for imports defined in `.editorconfig` property `ij_kotlin_packages_to_use_import_on_demand`. If this property is not explicitly set, it allows wildcards imports like `java.util.*` by default to keep in sync with IntelliJ IDEA behavior. + +## Can I use KtLint to directly format the code I'm generating with KotlinPoet? + +Yes, it is possible to use KtLint to directly format the code generated with KotlinPoet. +To do so, you must include the dependencies `com.pinterest.ktlint:ktlint-core` and `com.pinterest.ktlint:ktlint-ruleset-standard` in your Gradle/Maven project. + +!!! warning + Do not include the dependency `com.pinterest:ktlint` as that would import the entire ktlint project including unwanted dependencies. Besides a much bigger artifact, it might also result in problems regarding logging. + +To format the output of KotlinPoet with KtLint, you can use the following snippet: + +```kotlin +val ruleProviders = buildSet { + ServiceLoader + .load(RuleSetProviderV2::class.java) + .flatMapTo(this) { it.getRuleProviders() } +} +val ktLintRuleEngine = KtLintRuleEngine( + ruleProviders = ruleProviders, + editorConfigDefaults = EditorConfigDefaults.load(EDITORCONFIG_PATH), +) +ktLintRuleEngine.format(outputDir.toPath()) +``` +Here, outputDir refers to the directory of the generated files by KotlinPoet, ktLintRuleEngine is an instance of KtLint rule engine. + +It is also possible to format file-by-file the output of KotlinPoet if you write your `FileSpec` to a `StringBuilder()`, instead of a `File`, +and send the generated code as `String` to KtLint inside a `CodeSnippet`: +```kotlin +kotlinFile.writeText( + ktLintRuleEngine.format( + Code.CodeSnippet( + stringBuilder.toString() + ) + ) +) +``` + +# Are formatter tags respected? + +As of version `0.49.x` the formatter tags of IntelliJ IDEA are respected. By default, those formatter tags are disabled. The formatter tags can be enabled with `.editorconfig` properties below: +```editorconfig +ij_formatter_tags_enabled = true # Defaults to 'false' +ij_formatter_off_tag = some-custom-off-tag # Defaults to '@formatter:off' +ij_formatter_on_tag = some-custom-on-tag # Defaults to '@formatter:on' +``` + +When enabled, the ktlint rule checking is disabled for all code surrounded by the formatter tags. diff --git a/documentation/snapshot/docs/index.md b/documentation/snapshot/docs/index.md new file mode 100644 index 0000000000..85e6cecc62 --- /dev/null +++ b/documentation/snapshot/docs/index.md @@ -0,0 +1,41 @@ +# Welcome to Ktlint + +

+ + + +

+

+Join the chat at https://kotlinlang.slack.com +Build status +Maven Central +ktlint +

+

+Kotlin linter in spirit of feross/standard (JavaScript) and gofmt (Go). +

+ +## Features + +- **No configuration required** + `ktlint` aims to capture the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html) and [Android Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html). In some aspects `ktlint` is a bit more strict[*](https://github.com/pinterest/ktlint/issues/284#issuecomment-425177186). +- **Rule sets** + `ktlint` offers a `standard` rule set. Next to this, it is easy to provide [custom rule sets](api/custom-rule-set/). +- **.editorconfig** + Some rules do allow further configuration, but in all cases a reasonable default is set when not provided. `ktlint` primarily uses the [.editorconfig file](rules/configuration-ktlint/) to read default `.editorconfig`, IntelliJ IDEA specific and Ktlint specific properties. +- **Disable rules** + If need be, rules can be disabled easily[*](faq/#how-do-i-globally-disable-a-rule). +- **Built-in formatter** + Most lint violations don't need to be fixed manually. `ktlint` has a built-in formatter which fixes violations when possible. Some violations can not be fixed in a deterministic way, and need manual action. +- **Customizable output** + Several reporters are available out-of-the-box: `plain` (+ `plain?group_by_file`), `plain-summary`, `json`, `html` and `checkstyle`. + It's also easy to [create a custom reporter](api/custom-reporter/). +- **Executable jar** + `ktlint` is released as a single executable jar with all dependencies included. + +## Legal + +This project is not affiliated with nor endorsed by JetBrains. +All code, unless specified otherwise, is licensed under the [MIT](https://opensource.org/licenses/MIT) license. +Copyright (c) 2019 Pinterest, Inc. +Copyright (c) 2016-2019 Stanley Shyiko. diff --git a/documentation/snapshot/docs/install/cli.md b/documentation/snapshot/docs/install/cli.md new file mode 100644 index 0000000000..69d8457e78 --- /dev/null +++ b/documentation/snapshot/docs/install/cli.md @@ -0,0 +1,222 @@ +!!! note Command Line usage + If you don't plan to use `ktlint`'s command line interface then you can skip this section. + +## Download and verification + +### Download manually from github + +All releases of `ktlint` can be downloaded from the [releases](https://github.com/pinterest/ktlint/releases) page. + +### Download using curl + +A particular version of `ktlint` can be downloaded with next command which also changes the file to an executable in directory `/usr/local/bin`: + +```sh title="Download" +curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ +``` + +!!! tip "Curl not installed or behind proxy" + If you don't have curl installed - replace `curl -sL` with `wget -qO-`. + If you are behind a proxy see - [curl](https://curl.haxx.se/docs/manpage.html#ENVIRONMENT) / [wget](https://www.gnu.org/software/wget/manual/wget.html#Proxies) manpage. Usually simple: + ```shell + http_proxy=http://proxy-server:port https_proxy=http://proxy-server:port curl -sL ... + ``` + +### Verification of download + +`ktlint.asc` contains PGP signature which you can verify with: + +```sh title="Verify releases 0.32.0 and above" +curl -sS https://keybase.io/ktlint/pgp_keys.asc | gpg --import && gpg --verify ktlint.asc +``` + +```sh title="Verify releases up through 0.31.0" +curl -sS https://keybase.io/shyiko/pgp_keys.asc | gpg --import && gpg --verify ktlint.asc +``` + +### Package managers + +`ktlint` can be installed via several OS specific package managers. + +Install with [brew on macOS](https://brew.sh/) or [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux) +```sh +brew install ktlint +``` + +Install with [MacPorts](https://www.macports.org/) +```sh +port install ktlint +``` + +Install with [SDKMAN! on macOS and Linux](https://sdkman.io/) +```sh +sdk install ktlint +``` + +On Arch Linux install package [ktlint AUR](https://aur.archlinux.org/packages/ktlint/). + +## Command line usage + +### Rule set(s) + +When no arguments are specified, the style of all Kotlin files (ending with '.kt' or '.kts') inside the current dir (recursively) are validated with the (non-experimental) rules from the [standard ruleset](../../rules/standard/). Hidden folders will be skipped. + +```shell title="Default validation with standard ruleset" +ktlint +``` + +To validate with the [standard ruleset](../../rules/standard/) including the experimental rules run command below: + +```shell title="Validation with standard ruleset including the experimental rules" +ktlint --experimental +``` + +!!! note + Instead of using this command line flag, it is advised to set `.editorconfig` property `ktlint_experimental = enabled` if you want the project always to be checked with the experimental rules. + +To validate with a [custom ruleset](../../api/custom-rule-set/) run command below: + +```shell title="Validation with standard and a custom ruleset" +ktlint --ruleset=/path/to/custom-ruleset.jar +# or +ktlint -R /path/to/custom-ruleset.jar +``` + +!!! note + If the custom rule set contains rules that are marked as experimental, those rule will only be run when `.editorconfig` property `ktlint_experimental = enabled` is set (or command line parameter `--experimental` is specified). + +### Format (autocorrect) + +Most style violations can be corrected automatically. Errors that can not be corrected, are printed to `stderr`. + +```shell title="Autocorrect style violations" +ktlint --format +# or +ktlint -F +``` + +### Globs + +Globs can be used to specify more exactly what files and directories are to be validated. `ktlint` uses the [`.gitignore` pattern style syntax for globs](https://git-scm.com/docs/gitignore). Globs are processed from left to right. Prepend a glob with `!` to negate it. Hidden folders will be skipped. + +```shell title="Check only certain locations starting from the current directory" +# Check all '.kt' files in 'src/' directory, but ignore files ending with 'Test.kt': +ktlint 'src/**/*.kt' '!src/**/*Test.kt' + +# Check all '.kt' files in 'src/' directory, but ignore 'generated' directory and its subdirectories: +ktlint 'src/**/*.kt' '!src/**/generated/**' +``` + +### Violation reporting + +`ktlint` supports different type of reporters for lint violations. When not specified the `plain` reporter is used. Optionally the `plain` reporter can group the violations per file. + +```shell title="Style violation grouped by file" +$ ktlint --reporter=plain?group_by_file +``` + +When using `ktlint` on an existing project, the number of violations can be huge. To get more insights in which rules are causing the most violations, the `plain-summary` reporter can be used. +```shell title="Style violations counted per rule" +$ ktlint --reporter=plain-summary +``` + +Other built-in reporters are: `json`, `sarif`, `checkstyle`, and `html` + +Style violations can be written to an output file which is convenient when multiple reporters are specified. In example below, the plain reporter is used to write to the console while the checkstyle reports is written to a file: + +```shell title="Multiple reporters" +ktlint --reporter=plain --reporter=checkstyle,output=ktlint-report-in-checkstyle-format.xml +``` + +If resolving all existing errors in a project is unwanted, it is possible to create a baseline and in following invocations compare violations against this baseline. Violations that are registered in the baseline, will be ignored silently. Remove the baseline file in case you want to reset it. + +```shell title="Check against a baseline file" +ktlint --baseline=ktlint-baseline.xml # Baseline is created when not existing +``` + +### Logging + +Logging information is written to `stdout`. The amount of logging can be influenced by setting the minimal log level using option `--log-level` or `-l` to one of values `trace`, `debug`, `info`, `warn`, `error`, or `none` to suppress all logging. + +By default, the `info` log level is used meaning that all log lines at level `info`, `warn` and `error` are shown while suppressing log lines at level `debug` or `trace`. + +### Rule configuration (`.editorconfig`) + +Some rules can be tweaked via the [`editorconfig file`](../../rules/configuration-ktlint/). + +A scaffold of the `.editorconfig file` can be generated with command below. Note: that the generated file only contains configuration settings which are actively used by the [rules which are loaded](#rule-sets): + +```shell title="Generate .editorconfig" +ktlint generateEditorConfig +# or +ktlint --experimental generateEditorConfig +# or +ktlint --experimental --ruleset=/path/to/custom-ruleset.jar generateEditorConfig +``` + +Normally this file is located in the root of your project directory. In case the file is located in a sub folder of the project, the settings of that file only applies to that subdirectory and its folders (recursively). Ktlint automatically detects and reads all `.editorconfig` files in your project. + +Use command below, to specify a default `editorconfig`. In case a property is not defined in any `.editorconfig` file on the path to the file, the value from the default file is used. The path may point to any valid file or directory. The path can be relative or absolute. Depending on your OS, the "~" at the beginning of a path is replaced by the user home directory. + +```shell title="Override '.editorconfig'" +ktlint --editorconfig=/path/to/.editorconfig +``` + +!!! warning "Overrides '.editorconfig' in project directory" in KtLint 0.46 and older + When specifying this option using ktlint 0.46 or older, all `.editorconfig` files in the project directory are being ignored. Starting from KtLint 0.47 the properties in this file are used as fallback. + +### Stdin && stdout + +With command below, the input is read from `stdin` and the violations are printed to `stderr`. Logging is written to `stdout`. + +```shell title="Lint from stdin" +ktlint --stdin +``` + +When combined with the `--format` option, the formatted code is written to `stdout` and the violations are printed to `stderr`: + +```shell title="Format from stdin and write to stdout" +ktlint --stdin -F +``` + +!!! tip Suppress logging and error output + Logging output printed to `stdout` can be suppressed by setting `--log-level=none` (see [logging](#logging)). + Output printed to `stderr` can be suppressed in different ways. To ignore all error output, add `2> /dev/null` to the end of the command line. Otherwise, specify a [reporter](#violation-reporting) to write the error output to a file. + + +### Git hooks + +Predefined git hooks can be installed, to automatically validate lint errors before commit or push. + +```shell title="Install git pre-commit hook" +ktlint installGitPreCommitHook +``` + +```shell title="Install git pre-push hook" +ktlint installGitPrePushHook +``` + +### Miscellaneous flags and commands + +`-a` or `--android`: Turn on Android Kotlin Style Guide compatibility. This flag is most likely to be removed in a future version. Use [`.editorconfig ktlint_code_style`](../../rules/configuration-ktlint/#code-style). + +`--color` and `--color-name=`: Make output colorful and optionally set the color name to use. + +`--disabled_rules=`: A comma-separated list of rules to globally disable. To disable the standard ktlint rule-set use `--disabled_rules=standard`. This flag is most likely to be removed in a future version. Use [`.editorconfig disabled_rules`](../../rules/configuration-ktlint/#disabled-rules). + +`-h` or `--help`: Prints help information. + +`--limit=`: Maximum number of errors to show (default: show all) + +`--relative`: Print files relative to the working directory (e.g. dir/file.kt instead of /home/user/project/dir/file.kt) + +`--patterns-from-stdin[=]`: Reads additional patterns from `stdin`, where the patterns are separated by ``. If `=` is omitted, newline is used as fallback delimiter. If an empty string is given, the `NUL` byte is used as delimiter instead. +If this option is given, then the default patterns are disabled. +Options `--stdin` and `--patterns-from-stdin` are mutually exclusive, only one of them can be given at a time. + +`-V` or `--version`: Prints version information and exit. + +### Microsoft Windows users + +!!! tip "Microsoft Windows" + On Microsoft Windows you'll have to use `java -jar ktlint ...`. diff --git a/documentation/snapshot/docs/install/index.md b/documentation/snapshot/docs/install/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/snapshot/docs/install/integrations.md b/documentation/snapshot/docs/install/integrations.md new file mode 100644 index 0000000000..854077e597 --- /dev/null +++ b/documentation/snapshot/docs/install/integrations.md @@ -0,0 +1,225 @@ +## [Maven](https://github.com/shyiko/mvnw) integration + +By adding the plugin definition below to the `` section in the `pom.xml`: + +* The `ktlint` task is bound to the *Maven verify* lifecycle and will be executed each time the `mvn verify` is executed. It can also be executed with command `mvn antrun:run@ktlint`. +* The `ktlint-format` task is not bound to any other maven lifecycle. It can be executed with command `mvn antrun:run@ktlint-format`. + +See [cli usage](../cli) for arguments that can be supplied to `ktlint`. + +```xml title="Adding plugin to pom.xml" +... + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + ktlint + verify + + + + + + + + + + + run + + + + ktlint-format + + + + + + + + + + + + + run + + + + + + com.pinterest + ktlint + 0.49.1 + + + + +... +``` + +!!! Tip + If you want ktlint to run before code compilation takes place - change `verify` to `validate` (see [Maven Build Lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) for more). + +!!! Info "ktlint-maven-plugin" + You might be interested to use the dedicated [gantsign/ktlint-maven-plugin](https://github.com/gantsign/ktlint-maven-plugin). + +## [Gradle](https://gradle.org/) integration + +### jlleitschuh/ktlint-gradle + +The [jlleitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) Gradle plugin automatically creates check and format tasks for project Kotlin sources. It supports different kotlin plugins and Gradle build caching. + +### jeremymailen/kotlinter-gradle + +The [jeremymailen/kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) Gradle plugin features incremental build support, file reports, and `*.kts` source support. + +### diffplug/spotless + +The [diffplug/spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle#applying-ktlint-to-kotlin-files) Gradle plugin is a general-purpose formatting plugin which amongst many others also supports `ktlint`. + +### autostyle/autostyle + +The [autostyle/autostyle](https://github.com/autostyle/autostyle/tree/master/plugin-gradle#applying-ktlint-to-kotlin-files) Gradle plugin is a general-purpose formatting plugin which amongst others also supports `ktlint`. + +### Custom Gradle integration + +#### Custom Gradle integration with Groovy + +!!! Warning + It is recommended to use one of the Gradle plugins mentioned before. + +The configuration below, defines following task: + +* The `ktlintCheck` is bound to the *Gradle check* task. It can also be executed with command `./gradlew ktlintCheck`. +* The `ktlintFormat` task is not bound to any other task. It can be executed with command `./gradlew ktlintFormat`. + +```groovy title="build.gradle" +// kotlin-gradle-plugin must be applied for configuration below to work +// (see https://kotlinlang.org/docs/reference/using-gradle.html) + +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +configurations { + ktlint +} + +dependencies { + ktlint("com.pinterest:ktlint:0.49.1") { + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL)) + } + } + // additional 3rd party ruleset(s) can be specified here + // just add them to the classpath (e.g. ktlint 'groupId:artifactId:version') and + // ktlint will pick them up +} + +tasks.register("ktlintCheck", JavaExec) { + group = "verification" + description = "Check Kotlin code style." + classpath = configurations.ktlint + mainClass = "com.pinterest.ktlint.Main" + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args "src/**/*.kt", "**.kts", "!**/build/**" +} + +tasks.named("check") { + dependsOn tasks.named("ktlintCheck") +} + +tasks.register("ktlintFormat", JavaExec) { + group = "formatting" + description = "Fix Kotlin code style deviations." + classpath = configurations.ktlint + mainClass = "com.pinterest.ktlint.Main" + jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED" + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args "-F", "src/**/*.kt", "**.kts", "!**/build/**" +} +``` + +See [Making your Gradle tasks incremental](https://proandroiddev.com/making-your-gradle-tasks-incremental-7f26e4ef09c3) by [Niklas Baudy](https://github.com/vanniktech) on how to make tasks above incremental. + +#### Custom Gradle integration with Kotlin DSL + +!!! Warning + It is recommended to use one of the Gradle plugins mentioned before. + +The configuration below, defines following task: + +* The `ktlintCheck` is bound to the *Gradle check* task. It can also be executed with command `./gradlew ktlintCheck`. +* The `ktlintFormat` task is not bound to any other task. It can be executed with command `./gradlew ktlintFormat`. + +```kotlin title="build.gradle.kts" +val ktlint by configurations.creating + +dependencies { + ktlint("com.pinterest:ktlint:0.49.1") { + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + } + } + // ktlint(project(":custom-ktlint-ruleset")) // in case of custom ruleset +} + +val ktlintCheck by tasks.registering(JavaExec::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Check Kotlin code style" + classpath = ktlint + mainClass.set("com.pinterest.ktlint.Main") + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args( + "**/src/**/*.kt", + "**.kts", + "!**/build/**", + ) +} + +tasks.check { + dependsOn(ktlintCheck) +} + +tasks.register("ktlintFormat") { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Check Kotlin code style and format" + classpath = ktlint + mainClass.set("com.pinterest.ktlint.Main") + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args( + "-F", + "**/src/**/*.kt", + "**.kts", + "!**/build/**", + ) +} +``` + +## [GNU Emacs](https://www.gnu.org/software/emacs/) integration + +See [whirm/flycheck-kotlin](https://github.com/whirm/flycheck-kotlin). + +## [Vim](https://www.vim.org/) integration + +See [w0rp/ale](https://github.com/w0rp/ale). + +## [Mega-Linter](https://nvuillam.github.io/mega-linter/) integration + +The [Mega-Linter](https://nvuillam.github.io/mega-linter/) integrates 70+ linters in a single tool for CI, including **ktlint** activated out of the box + +## Other integration + +Do you know any other integration with `ktlint` then please create a PR to add this integration to our documentation. diff --git a/documentation/snapshot/docs/install/overview.md b/documentation/snapshot/docs/install/overview.md new file mode 100644 index 0000000000..ac1b7a343b --- /dev/null +++ b/documentation/snapshot/docs/install/overview.md @@ -0,0 +1,6 @@ +See [command line interface](cli.md) or [integrations](integrations.md) for details on installing the latest release of `ktlint`. + +## Online demo + +See [`ktlint` online](https://saveourtool.com/#/demo/diktat) if you want to try-out 'ktlint'. This online version compares rule sets provided by `ktlint` and `diktat` (a layer on top of `ktlint`). To contribute to or get more info about `ktlint` online, please visit the [GitHub repository](https://github.com/saveourtool/diktat-demo). + diff --git a/documentation/snapshot/docs/install/snapshot-build.md b/documentation/snapshot/docs/install/snapshot-build.md new file mode 100644 index 0000000000..cbd594486d --- /dev/null +++ b/documentation/snapshot/docs/install/snapshot-build.md @@ -0,0 +1,37 @@ +## Access to the latest `master` snapshot + +Whenever a commit is added to the `master` branch a snapshot build is automatically uploaded to [Sonatype's snapshots repository](https://oss.sonatype.org/content/repositories/snapshots/com/pinterest/ktlint/). +If you are eager to try upcoming changes (that might or might not be included in the next stable release) you can do +so by changing version of ktlint to `-SNAPSHOT` + adding a repo: + +### Maven + +```xml +... + + sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + false + + +... +``` + +### Gradle + +```groovy +repositories { + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } +} +``` + +### Kotlin development version snapshot + +Additionally, project publishes snapshots build against latest kotlin development version. To use them, change version +of ktlint to `-kotlin-dev-SNAPSHOT`. diff --git a/documentation/snapshot/docs/readme.md b/documentation/snapshot/docs/readme.md new file mode 100644 index 0000000000..8c710bb188 --- /dev/null +++ b/documentation/snapshot/docs/readme.md @@ -0,0 +1,32 @@ +# Build & test documentation on local machine + +The documentation of ktlint is served with [mkdocs-material](https://squidfunk.github.io/mkdocs-material/creating-your-site/#advanced-configuration). For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +To build and test documentation on your local development machine, follow steps below: + +## Setup +1. In IntelliJ IDEA + * Open `Preferences` + * Search for `JSON Schema mappings` + * Add new schema for url `https://squidfunk.github.io/mkdocs-material/schema.json` and add file `mkdocs.yml` for this url. +2. Pull docker image + ```shell + $ docker pull squidfunk/mkdocs-material + ``` + +## Build server +The following steps build and host the documentation locally, updating automatically whenever a local file is changed. + +1. Start mkdocs server from root of project (e.g. from same directory where file mkdocs.yml is located) + ```shell + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + ``` +2. Visit page `http://0.0.0.0:8000/` in your browser. +3. Edit the documentation and explicitly save the file. The mkdocs server refreshes its cached and the current page in the browser is automatically refreshed. + +## Build once +If you do not want to run a local server, or if you want to inspect the built files, you can run the following command from the project's main directory to build the documentation in the `site/` directory. + +```shell +docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material build +``` diff --git a/documentation/snapshot/docs/rules/code-styles.md b/documentation/snapshot/docs/rules/code-styles.md new file mode 100644 index 0000000000..bf41a58463 --- /dev/null +++ b/documentation/snapshot/docs/rules/code-styles.md @@ -0,0 +1,17 @@ +In ktlint `0.49` the new code style `ktlint_official` is introduced. This code style is work in progress but will become the default code style in the `1.0` release. Please try out the new code style and provide your feedback via the [issue tracker](https://github.com/pinterest/ktlint/issues). + +```ini +[*.{kt,kts}] +ktlint_code_style = ktlint_official +``` + +This `ktlint_official` code style combines the best elements from the [Kotlin Coding conventions](https://kotlinlang.org/docs/coding-conventions.html) and [Android's Kotlin styleguide](https://developer.android.com/kotlin/style-guide). This code style also provides additional formatting on topics which are not (explicitly) mentioned in those conventions and style guide. + +!!! note + Be aware that this code style in some cases formats code in a way which is not accepted by the default code formatters in IntelliJ IDEA and Android Studio. The formatters of those editors produce nicely formatted code in the vast majority of cases. But in a number of edge cases, the formatting contains bugs which are waiting to be fixed for several years. The new code style formats code in a way which is compatible with the default formatting of the editors whenever possible. When using this codestyle, it is best to disable (e.g. not use) code formatting in the editor. + +The existing code styles have been renamed to make more clear what the basis of the code style is. + +* The `official` code style has been renamed to `intellij_idea`. Code formatted with this code style aims to be compatible with default formatter of IntelliJ IDEA. This code style is based on [Kotlin Coding conventions](https://kotlinlang.org/docs/coding-conventions.html). If `.editorconfig` property `ktlint_code_style` has been set to `official` then do not forget to change the value of that property to `intellij_idea`. When not set, this is still the default code style of ktlint `0.49`. Be aware that the default code style will be changed to `ktlint_official` in the `1.0` release. + +* Code style `android` has been renamed to `android_studio`. Code formatted with this code style aims to be compatible with default formatter of Android Studio. This code style is based on [Android's Kotlin styleguide](https://developer.android.com/kotlin/style-guide). If `.editorconfig` property `ktlint_code_style` has been set to `android` then do not forget to change the value of that property to `android_studio`. diff --git a/documentation/snapshot/docs/rules/configuration-intellij-idea.md b/documentation/snapshot/docs/rules/configuration-intellij-idea.md new file mode 100644 index 0000000000..a7c570cfa4 --- /dev/null +++ b/documentation/snapshot/docs/rules/configuration-intellij-idea.md @@ -0,0 +1,36 @@ +!!! Warning + `ktlint` strives to prevent code formatting conflicts with IntelliJ IDEA / Android Studio. We recommend using either IDE formatting or `ktlint` formatting. However, if you persist on using both, then please ensure that the formatting settings are aligned as described below. This reduces the chance that code which is formatted by ktlint conflicts with formatting by the IntelliJ IDEA built-in formatter. + +!!! Note + IntelliJ IDEA supports the [kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html). As of version 0.47.x of ktlint, the support to overwrite some configuration files of IntelliJ IDEA has been dropped as it no longer fits the scope of the project. + + +Steps: + +1. Go to your project directory +2. Create or replace file `.idea/codeStyles/codeStyleConfig.xml` with content below: + ```xml + + + + + ``` +3. Create or replace file `.idea/codeStyles/Project.xml` with content below: + ```xml + + + + + + + + + + ``` diff --git a/documentation/snapshot/docs/rules/configuration-ktlint.md b/documentation/snapshot/docs/rules/configuration-ktlint.md new file mode 100644 index 0000000000..bd7533867f --- /dev/null +++ b/documentation/snapshot/docs/rules/configuration-ktlint.md @@ -0,0 +1,258 @@ +Ktlint uses a limited set of `.editorconfig` properties for additional configuration. A sensible default value is provided for each property when not explicitly defined. Properties can be overridden, provided they are specified under `[*.{kt,kts}]`. Ktlint uses some properties defined by [.editorconfig](https://editorconfig.org/), IntelliJ IDEA and custom properties. + +!!! danger + + Unfortunately [IntelliJ IDEA](https://www.jetbrains.com/idea/) has an [autoformat issue regarding `.editorconfig`](https://youtrack.jetbrains.com/issue/IDEA-242506). Due to this error an additional space is added between glob statements, resulting in `[*{kt, kts}]` instead of `[*{kt,kts}]`. The `.editorconfig` library used by `ktlint` [ignores sections after encountering a space in the list](https://github.com/editorconfig/editorconfig/issues/148). As a result, the rule is not applied on all files as documented in the [original ktlint issue](https://github.com/pinterest/ktlint/issues/762). + +## Code style + +By default, the `intellij_idea` Kotlin code style is applied. Alternatively, the code style can be set to `ktlint_official` or `android`. + +```ini +[*.{kt,kts}] +ktlint_code_style = ktlint_official +``` + +!!! note + The default code style will be changed to `ktlint_official` in the `1.0` version of ktlint. + +## Disabled rules + +!!! note + Support of properties `disabled_rules` and `ktlint_disabled_rules` has been removed in KtLint `0.49`. + +Rule sets and individual rules can be disabled / enabled with a separate property per rule (set). + +All rules in a rule set can be enabled or disabled with a rule set property. The name of the rule set property consists of the `ktlint_` prefix followed by the rule set id. Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable all rules from the `experimental` rule set provided by KtLint +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +``` + +Rules that are marked as experimental will not be run, unless explicitly enabled: +```editorconfig +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +``` + +An individual rule can be enabled or disabled with a rule property. The name of the rule property consists of the `ktlint_` prefix followed by the rule set id followed by a `_` and the rule id. Examples: +```editorconfig +ktlint_standard_final-newline = disabled # Disables the `final-newline` rule provided by KtLint +ktlint_standard_some-experimental-rule = enabled # Enables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + + +## Final newline + +By default, a final newline is required at the end of the file. + +```ini +[*.{kt,kts}] +insert_final_newline = true +``` + +This setting only takes effect when rule `final-newline` is enabled. + +## Force multiline function signature based on number of parameters + +Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` forces a multiline function signature in case the function contains the specified minimum number of parameters even in case the function signature would fit on a single line. Use value `unset` (default) to disable this setting. + +!!! note + By default, the `ktlint_official` code style wraps parameters of functions having at least 2 parameters. For other code styles, this setting is disabled by default. + +```ini +[*.{kt,kts}] +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than=unset +``` + +This setting only takes effect when rule `function-signature` is enabled. + +## Wrapping the expression body of a function + +Setting `ktlint_function_signature_body_expression_wrapping` determines if and when the expression body of a function is wrapped to a new line. This setting can be set to value `default`, `multiline` or `always`. + +When set to `default`, the first line of a body expression is appended to the function signature as long as the max line length is not exceeded. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=default (or when not set)" +// Given that the function signature has to be written as a single line function signature +fun someFunction(a: Any, b: Any): String = "some-result" + .uppercase() + +// Given that the function signature has to be written as a multiline function signature +fun someFunction( + a: Any, + b: Any +): String = "some-result" + .uppercase() +``` + +When set to `multiline`, the body expression starts on a separate line in case it is a multiline expression. A single line body expression is wrapped only when it does not fit on the same line as the function signature. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=multiline" +// Given a single line body expression and +// a the function signature that has to be written as a single line function signature and +// it does not exceed the max line length +fun someFunction(a: Any, b: Any): String = "some-result".uppercase() + +// Given a single line body expression and +// a the function signature that has to be written as a multiline function signature and +// it does not exceed the max line length +fun someFunction( + a: Any, + b: Any +): String = "some-result".uppercase() + +// Given a single line body expression then always wrap it to a separate line +fun someFunction(a: Any, b: Any): String = + "some-result" + .uppercase() +fun someFunction( + a: Any, + b: Any +): String = + "some-result" + .uppercase() +``` + +When set to `always` the body expression is always wrapped to a separate line. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=always" +fun someFunction(a: Any, b: Any): String = + "some-result".uppercase() +fun functionWithAVeryLongName( + a: Any, + b: Any +): String = + "some-result" + .uppercase() +``` + +This setting only takes effect when rule `function-signature` is enabled. + +## Ignore identifiers enclosed in backticks + +By default, the identifiers enclosed in backticks are not ignored. + +According to [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#names-for-test-methods) it is acceptable to write method names in natural language. When using natural language, the description tends to be longer. This property allows lines containing an identifier between backticks to be longer than the maximum line length. (Since 0.41.0) + +```kotlin +@Test +fun `Given a test with a very loooooooooooooooooooooong test description`() { + +} +``` + +```ini +[*.{kt,kts}] +ktlint_ignore_back_ticked_identifier = false +``` + +This setting only takes effect when rule `max-line-length` is enabled. + +## Import layouts + +By default, the same imports are allowed as in IntelliJ IDEA. The import path can be a full path, e.g. "java.util.List.*" as well as wildcard path, e.g. "kotlin.**". + +The layout can be composed by the following symbols: + +* `*` - wildcard. There must be at least one entry of a single wildcard to match all other imports. Matches anything after a specified symbol/import as well. +* `|` - blank line. Supports only single blank lines between imports. No blank line is allowed in the beginning or end of the layout. +* `^` - alias import, e.g. "^android.*" will match all android alias imports, "^" will match all other alias imports. + +Examples: +```kotlin +ij_kotlin_imports_layout=* # alphabetical with capital letters before lower case letters (e.g. Z before a), no blank lines +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ # default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list +ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # custom imports layout +``` + +Wildcard imports can be allowed for specific import paths (Comma-separated list, use "**" as wildcard for package and all subpackages). This setting overrides the no-wildcard-imports rule. This setting is best be used for allowing wildcard imports from libraries like Ktor where extension functions are used in a way that creates a lot of imports. + +```ini +[*.{kt,kts}] +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.** +``` + +This setting only takes effect when rule `no-wildcard-imports` is enabled. + +## Indent size & style + +By default, indenting is done with 4 spaces per indent level. Code style `android_studio` uses a tab per indent level. + +```ini +[*.{kt,kts}] +indent_size = 4 # possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely) +indent_style = space # or "tab" +``` + +Those settings are used by multiple rules of which rule `indent` is the most important. + +## Max line length + +By default, the maximum line length is not set. The `android` code style sets the max line length to 100 (per Android Kotlin Style Guide). + +```ini +[*.{kt,kts}] +max_line_length = off # Use "off" to ignore max line length or a positive number to set max line length +``` + +This setting is used by multiple rules of which rule `max-line-length` is the most important. + +## Trailing comma on call site + +KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma_on_call_site` to *enforce* the usage of the trailing comma at call site when enabled. IntelliJ IDEA uses this property to *allow* the use of trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on call site has been changed to `true` except when codestyle `android` is used. + + Although the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) leaves it to the developer's discretion to use trailing commas on the call site, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +Example: +```ini +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma_on_call_site = false +``` + +This setting only takes effect when rule `trailing-comma-on-call-site` is enabled. + +## Trailing comma on declaration site + +KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma` to *enforce* the usage of the trailing comma at declaration site when enabled. IntelliJ IDEA uses this property to *allow* the use of trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on declaration site has been changed to `true` except when codestyle `android` is used. + + The [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) encourages the usage of trailing commas on the declaration site, but leaves it to the developer's discretion to use trailing commas on the call site. But next to this, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +Example: +```ini +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = false # Only used for declaration site +``` + +This setting only takes effect when rule `trailing-comma-on-declaration-site` is enabled. + +## Overriding Editorconfig properties for specific directories + +You can [override](https://editorconfig.org/#file-format-details) properties for specific directories inside your project: +```ini +[*.{kt,kts}] +ktlint_standard_import-ordering = disabled + +[api/*.{kt,kts}] +ktlint_standard_indent = disabled +``` + +Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. diff --git a/documentation/snapshot/docs/rules/dependencies.md b/documentation/snapshot/docs/rules/dependencies.md new file mode 100644 index 0000000000..823a4675f0 --- /dev/null +++ b/documentation/snapshot/docs/rules/dependencies.md @@ -0,0 +1,3 @@ +Preferably rules run independent of each other. In some case this is however not feasible. The diagram below shows the dependencies between the rules provided by KtLint. + +![Image](../../assets/images/rule-dependencies.png) diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md new file mode 100644 index 0000000000..8253fbc510 --- /dev/null +++ b/documentation/snapshot/docs/rules/experimental.md @@ -0,0 +1,803 @@ +!!! important + Experimental rules in ktlint are part of the [standard ruleset](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-standard). Experimental rules are run only when `.editorconfig` property `ktlint_experimental` is enabled. Or, when a specific experimental rule is enabled via `.editorconfig` property `ktlint__`. + +## Discouraged comment location + +Detect discouraged comment locations (no autocorrect). + +!!! note + Kotlin allows comments to be placed almost everywhere. As this can lead to code which is hard to read, most of them will never be used in practice. Ideally each rule takes comments at all possible locations into account. Sometimes this is really hard and not worth the effort. By explicitly forbidding such comment locations, the development of those rules becomes a bit easier. + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun /* some comment */ foo(t: T) = "some-result" + + fun foo() { + if (true) + // some comment + bar() + } + ``` + +Rule id: `discouraged-comment-location` + +## Disallow empty lines at start of class body + +Detect blank lines at start of a class body. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo { + val foo = "foo" + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class Foo { + + val foo = "foo" + } + ``` + +Rule id: `no-empty-first-line-in-class-body` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Disallow consecutive comments + +Consecutive comments are disallowed in following cases: +- Any mix of a consecutive kdoc, a block comment or an EOL comment unless separated by a blank line in between +- Consecutive KDocs (even when separated by a blank line) +- Consecutive block comments (even when separated by a blank line) + +Consecutive EOL comments are always allowed as they are often used instead of a block comment. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // An EOL comment + // may be followed by another EOL comment + val foo = "foo" + + // Different comment types (including KDoc) may be consecutive .. + + /* + * ... but do need to be separated by a blank line ... + */ + + /** + * ... but a KDoc can not be followed by an EOL or a block comment or another KDoc + */ + fun bar() = "bar" + + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + /* + * Block comments can not be consecutive ... + */ + /* + * ... even not when separated by a new line. + */ + val bar = "bar" + + /** + * A KDoc can not be followed by a block comment or an EOL comment or another KDOC + */ + + // ... even not when separated by a new line. + ``` + +Rule id: `no-consecutive-comments` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Function signature + +Rewrites the function signature to a single line when possible (e.g. when not exceeding the `max_line_length` property) or a multiline signature otherwise. In case of function with a body expression, the body expression is placed on the same line as the function signature when not exceeding the `max_line_length` property. Optionally the function signature can be forced to be written as a multiline signature in case the function has more than a specified number of parameters (`.editorconfig` property `ktlint_function_signature_wrapping_rule_always_with_minimum_parameters`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + fun foooooooo( + a: Any, + b: Any, + c: Any + ): String { + // body + } + + // Assume that the last allowed character is + // at the X character on the right X + fun bar(a: Any, b: Any, c: Any): String { + // body + } + + // When wrapping of body is set to 'default'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = "some-result" + .uppercase() + + // When wrapping of body is set to 'multiline' + // or 'always'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = + "some-result" + .uppercase() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + fun foooooooo(a: Any, b: Any, c: Any): String { + // body + } + + // Assume that the last allowed character is + // at the X character on the right X + fun bar( + a: Any, + b: Any, + c: Any + ): String { + // body + } + + // When wrapping of body is set to 'default'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = + "some-result" + .uppercase() + + // When wrapping of body is set to 'multiline' + // or 'always'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = "some-result" + .uppercase() + ``` + +Rule id: `function-signature` + +## If else bracing + +If at least one branch of an if-else statement or an if-else-if statement is wrapped between curly braces then all branches should be wrapped between braces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(value: int) { + if (value > 0) { + doSomething() + } else if (value < 0) { + doSomethingElse() + } else { + doSomethingElse2() + } + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foo(value: int) { + if (value > 0) + doSomething() + else if (value < 0) { + doSomethingElse() + } else + doSomethingElse2() + } + ``` + +Rule id: `if-else-bracing` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## If else wrapping + +A single line if-statement should be kept simple. It may contain no more than one else-branch. The branches may not be wrapped in a block. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foobar() { + if (true) foo() + if (true) foo() else bar() + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foobar() { + if (true) if (false) foo() else bar() + if (true) bar() else if (false) foo() else bar() + if (true) { foo() } else bar() + if (true) bar() else { if (false) foo() else bar() } + } + ``` + +Rule id: `if-else-wrapping` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Naming + +### Function naming + +Enforce naming of function. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() {} + fun fooBar() {} + ``` +=== "[:material-heart:](#) Ktlint Test" + + ```kotlin + @Test + fun `Some name`() {} + + @Test + fun do_something() {} + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun Foo() {} + fun Foo_Bar() {} + fun `Some name`() {} + fun do_something() {} + ``` + +!!! note + Functions in files which import a class from package `org.junit`, `org.testng` or `kotlin.test` are considered to be test functions. Functions in such classes are allowed to have underscores in the name. Or function names can be specified between backticks and do not need to adhere to the normal naming convention. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `FunctionName`. + +Rule id: `function-naming` + +### Package naming + +Enforce naming of package. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `PackageName`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package foo + package foo.foo + package foo_bar + package foo.foo_bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package Foo + package foo.Foo + package `foo bar` + package foo.`foo bar` + ``` + +Rule id: `package-naming` + +### Property naming + +Enforce naming of property. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val FOO_1 = Foo() + val FOO_BAR_1 = "FOO-BAR" + + var foo1: Foo = Foo() + + class Bar { + const val FOO_2 = "foo" + const val FOO_BAR_2 = "FOO-BAR" + + val foo2 = "foo" + val fooBar2 = "foo-bar" + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val Foo1 = Foo() + val FooBar1 = "FOO-BAR" + + var FOO_1: Foo = Foo() + + class Bar { + const val foo2 = "foo" + const val fooBar2 = "FOO-BAR" + + val FOO2 = "foo" + val FOO_BAR_2 = "foo-bar" + } + ``` + +!!! note + Top level `val` properties and `const val` properties have to be written in screaming snake notation. Local `val` and `const val` are written in lower camel case. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `PropertyName`. + +Rule id: `property-naming` + +## No empty file + +A kotlin (script) file should not be empty. It needs to contain at least one declaration. Files only contain a package and/or import statements are as of that disallowed. + +Rule id: `no-empty-file` + +## No single line block comments + +A single line block comment should be replaced with an EOL comment when possible. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * Some comment + */ + val foo = "foo" // Some comment + val foo = { /* no-op */ } + + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment */ + val foo = "foo" /* Some comment */ + ``` + +Rule id: `no-single-line-block-comment` + +## Spacing + +### No blank lines in list + +Disallow blank lines to be used in lists before the first element, between elements, and after the last element. + +*Super type* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class FooBar: + Foo, + Bar { + // body + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class FooBar: + + Foo, + + Bar + + { + // body + } + ``` + +*Type argument list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foobar: FooBar< + Foo, + Bar, + > = FooBar(Foo(), Bar()) + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + val foobar: FooBar< + + Foo, + + Bar, + + > = FooBar(Foo(), Bar()) + ``` + +*Type constraint list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class BiAdapter( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter() + where A1 : RecyclerView.Adapter, A1 : ComposableAdapter.ViewTypeProvider, + A2 : RecyclerView.Adapter, A2 : ComposableAdapter.ViewTypeProvider { + // body + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class BiAdapter( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter() + where + A1 : RecyclerView.Adapter, A1 : ComposableAdapter.ViewTypeProvider, + + A2 : RecyclerView.Adapter, A2 : ComposableAdapter.ViewTypeProvider + { + // body + } + ``` + +*Type parameter list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun < + Foo, + Bar, + > foobar() + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun < + + Foo, + + Bar, + + > foobar() + ``` + +*Value argument list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foobar = foobar( + "foo", + "bar", + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foobar = foobar( + + "foo", + + "bar", + + ) + ``` + +*Value parameter list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foobar( + foo: String, + bar: String, + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foobar( + + foo: String, + + bar: String, + + ) + ``` + +Rule id: `no-blank-line-in-list` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Parameter list spacing + +Consistent spacing inside the parameter list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(a: Any ) = "some-result" + fun foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo( a : Any ) = "some-result" + fun foo( + ) = "some-result" + ``` + +Rule id: `parameter-list-spacing` + +### String template indent + +Enforce consistent string template indentation for multiline string templates which are post-fixed with `.trimIndent()`. The opening and closing `"""` are placed on separate lines and the indentation of the content of the template is aligned with the `"""`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + """ + line1 + line2 + """.trimIndent() + fun foo() { + // The opening """ can not be wrapped to next line as that would result in a compilation error + return """ + line1 + line2 + """.trimIndent() + } + ``` +=== "[:material-heart:](#) Disallowed" + + ```kotlin + val foo = """ + line1 + line2 + """.trimIndent() + fun foo() { + return """ + line1 + line2 + """.trimIndent() + } + ``` + +Rule id: `string-template-indent` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Try catch finally spacing + +Enforce consistent spacing in `try { .. } catch { .. } finally { .. }`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = + try { + // do something + } catch (exception: Exception) { + // handle exception + } finally { + // clean up + } + ``` +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foo1() = try { /* ... */ } catch (exception: Exception) { /* ... */ } finally { /* ... */ } + fun foo2() = + try { + // do something + } + catch (exception: Exception) { + // handle exception + } + finally { + // clean up + } + ``` + +Rule id: `try-catch-finally-spacing` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Type argument list spacing + +Spacing before and after the angle brackets of a type argument list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val res = ArrayList() + class B : A() { + override fun x() = super.x() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val res = ArrayList < LintError > () + class B : A< T >() { + override fun x() = super< A >.x() + } + ``` + +Rule id: `type-argument-list-spacing` + +### Type parameter list spacing + +Spacing after a type parameter list in function and class declarations. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1(t: T) = "some-result" + fun foo2(t: T) = "some-result" + fun foo3(t: T) = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1(t: T) = "some-result" + fun foo2(t: T) = "some-result" + funfoo3(t: T) = "some-result" + ``` + +Rule id: `type-parameter-list-spacing` + +## Wrapping + +### Content receiver wrapping + +Wraps the content receiver list to a separate line regardless of maximum line length. If the maximum line length is configured and is exceeded, wrap the context receivers and if needed its projection types to separate lines. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // ALways wrap regardless of whether max line length is set + context(Foo) + fun fooBar() + + // Wrap each context receiver to a separate line when the + // entire context receiver list does not fit on a single line + context( + Fooooooooooooooooooo1, + Foooooooooooooooooooooooooooooo2 + ) + fun fooBar() + + // Wrap each context receiver to a separate line when the + // entire context receiver list does not fit on a single line. + // Also, wrap each of it projection types in case a context + // receiver does not fit on a single line after it has been + // wrapped. + context( + Foooooooooooooooo< + Foo, + Bar + > + ) + fun fooBar() + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Should be wrapped regardless of whether max line length is set + context(Foo) fun fooBar() + + // Should be wrapped when the entire context receiver list does not + // fit on a single line + context(Fooooooooooooooooooo1, Foooooooooooooooooooooooooooooo2) + fun fooBar() + + // Should be wrapped when the entire context receiver list does not + // fit on a single line. Also, it should wrap each of it projection + // type in case a context receiver does not fit on a single line + // after it has been wrapped. + context(Foooooooooooooooo) + fun fooBar() + ``` + +Rule id: `context-receiver-wrapping` + +## Enum wrapping + +An enum should be a single line, or each enum entry has to be placed on a separate line. In case the enumeration contains enum entries and declarations those are to be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + enum class Foo { A, B, C, D } + + enum class Foo { + A, + B, + C, + D, + ; + + fun foo() = "foo" + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + enum class Foo { + A, + B, C, + D + } + + enum class Foo { + A; + fun foo() = "foo" + } + ``` + +Rule id: `enum-wrapping` + +### Multiline expression wrapping + +Multiline expression on the right hand side of an expression are forced to start on a separate line. Expressions in return statement are excluded as that would result in a compilation error. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + foo( + parameterName = + "The quick brown fox " + .plus("jumps ") + .plus("over the lazy dog"), + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = foo( + parameterName = "The quick brown fox " + .plus("jumps ") + .plus("over the lazy dog"), + ) + ``` + +Rule id: `multiline-expression-wrapping` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. diff --git a/documentation/snapshot/docs/rules/index.md b/documentation/snapshot/docs/rules/index.md new file mode 100644 index 0000000000..ef8c375ea0 --- /dev/null +++ b/documentation/snapshot/docs/rules/index.md @@ -0,0 +1 @@ += Rules diff --git a/documentation/snapshot/docs/rules/standard.md b/documentation/snapshot/docs/rules/standard.md new file mode 100644 index 0000000000..3bf94be6e5 --- /dev/null +++ b/documentation/snapshot/docs/rules/standard.md @@ -0,0 +1,1424 @@ +## Annotation formatting + +Multiple annotations should be on a separate line than the annotated declaration; annotations with parameters should each be on separate lines; annotations should be followed by a space + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // A single annotation (without parameters) is allowed on same line as annotated construct + @FunctionalInterface class FooBar { + @JvmField var foo: String + @Test fun bar() {} + } + // A class or function parameter may have a single annotation with parameter(s) on the same line + class Foo(@Path("fooId") val fooId: String) + class Bar( + @NotNull("fooId") val fooId: String, + @NotNull("bar") bar: String + ) + // Multiple annotations (without parameters) are allowed on the same line + @Foo @Bar + class FooBar { + @Foo @Bar + var foo: String + @Foo @Bar + fun bar() {} + } + // An array of annotations (without parameters) is allowed on same line as annotated construct + @[Foo Bar] class FooBar2 { + @[Foo Bar] var foo: String + @[Foo Bar] fun bar() {} + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // An annotation with parameter(s) is not allowed on same line as annotated construct + @Suppress("Unused") class FooBar { + @Suppress("Unused") var foo: String + @Suppress("Unused") fun bar() {} + } + // Multiple annotation on same line as annotated construct are not allowed + @Foo @Bar class FooBar { + @Foo @Bar var foo: String + @Foo @Bar fun bar() {} + } + ``` + +Rule-id: `annotation` + +## Argument list wrapping + +All arguments should be on the same line, or every argument should be on a separate line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val x = f( + a, + b, + c + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val x = f( + a, + b, c + ) + ``` + +Rule-id: `argument-list-wrapping` + +## Block comment initial star alignment + +Lines in a block comment which (exclusive the indentation) start with a `*` should have this `*` aligned with the `*` in the opening of the block comment. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * This comment is formatted well. + */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* + * This comment is not formatted well. + */ + ``` + +Rule id: `block-comment-initial-star-alignment` + +## Chain wrapping + +When wrapping chained calls `.`, `?.` and `?:` should be placed on the next line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = listOf(1, 2, 3) + .filter { it > 2 }!! + .takeIf { it.count() > 100 } + ?.sum() + val foobar = foo() ?: + bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = listOf(1, 2, 3). + filter { it > 2 }!!. + takeIf { it.count() > 100 }?. + sum() + val foobar = foo() + ?: bar + ``` + +Rule id: `chain-wrapping` + +## Class/object naming + +Enforce naming of class. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo + class Foo1 + ``` +=== "[:material-heart:](#) Ktlint JUnit Test" + + ```kotlin + @Nested + inner class `Some descriptive class name` { + @Test + fun `Some descriptive test name`() { + // do something + } + } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class foo + class Foo_Bar + class `Some class in the production code` + ``` + +!!! note + Functions in files which import a class from package `org.junit.jupiter.api` are considered to be test functions and are allowed to have a name specified between backticks and do not need to adhere to the normal naming convention. Although, the [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) does not allow this explicitly for class identifiers, `ktlint` does allow it. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `ClassName`. + +Rule id: `class-naming` + +## Enum entry + +Enum entry names should be uppercase underscore-separated names. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + enum class Bar { + FOO, + Foo, + FOO_BAR, + Foo_Bar + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + enum class Bar { + foo, + bAr, + Foo_Bar, + } + ``` + +Rule id: `enum-entry-name-case` + +## File name + +A file containing only one visible (e.g. non-private) class, and visible declarations related to that class only, should be named according to that element. The same applies if the file does not contain a visible class but exactly one type alias or one object declaration. Otherwise, the PascalCase notation should be used. + +Rule id: `filename` + +## Final newline + +Ensures consistent usage of a newline at the end of each file. + +This rule can be configured with `.editorconfig` property [`insert_final_newline`](../configuration-ktlint/#final-newline). + +Rule id: `final-newline` + +## Import ordering + +Ensures that imports are ordered consistently (see [Import Layouts](../configuration-ktlint/#import-layouts) for configuration). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + import com.bar.Bar + import com.foo.Foo + import org.foo.bar.FooBar + import java.util.concurrent.ConcurrentHashMap + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + import com.bar.Bar + import java.util.concurrent.ConcurrentHashMap + import org.foo.bar.FooBar + import com.foo.Foo + ``` + +Rule id: `import-ordering` + +## Indentation + +Indentation formatting - respects `.editorconfig` `indent_size` with no continuation indent (see [EditorConfig](../configuration-ktlint/) section for more). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + foobar( + a, + b, + c + ) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + foobar( + a, + b, + c + ) + } + ``` + +!!! note + This rule handles indentation for many different language constructs which can not be summarized with a few examples. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. + +Rule id: `indent` + +## Max line length + +Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules then that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + // Lines below are accepted although the max + // line length is exceeded. + package com.toooooooooooooooooooooooooooo.long + import com.tooooooooooooooooooooooooooooo.long + val foo = + """ + fooooooooooooooooooooooooooooooooooooooooo + """ + @Test + fun `Test description which is toooooooooooo long`() { + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val fooooooooooooooo = "fooooooooooooooooooooo" + val foooooooooooooo = "foooooooooooooooooooo" // some comment + val fooooooooooooo = + "foooooooooooooooooooooooooooooooooooooooo" + ``` + +Rule id: `max-line-length` + +## Modifier order + +Consistent order of modifiers + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + abstract class A { + protected open val v = "" + internal open suspend fun f(v: Any): Any = "" + protected lateinit var lv: String + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + abstract class A { + open protected val v = "" + open suspend internal fun f(v: Any): Any = "" + lateinit protected var lv: String + } + ``` + +Rule id: `modifier-order` + +## Multiline if-else + +Braces required for multiline if/else statements. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + if (true) { + return 0 + } else { + return 1 + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = + if (true) + return 0 + else + return 1 + ``` + +Rule id: `multiline-if-else` + +## No blank lines before `}` + +No blank lines before `}`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + fun a() { + } + fun b() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + fun a() { + + } + fun b() + + } + ``` + +Rule id: `no-blank-line-before-rbrace` + +## No blank lines in chained method calls + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(inputText: String) { + inputText + .lowercase(Locale.getDefault()) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo(inputText: String) { + inputText + + .lowercase(Locale.getDefault()) + } + ``` + +Rule id: `no-blank-lines-in-chained-method-calls` + +## No consecutive blank lines + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package com.test + + import com.test.util + + val a = "a" + + fun b() { + } + + fun c() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package com.test + + + import com.test.util + + + val a = "a" + + + fun b() { + } + + + fun c() + ``` + +Rule id: `no-consecutive-blank-lines` + +## No empty (`{}`) class bodies + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class C + data class DC(val v: Any) + interface I + object O + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class C {} + data class DC(val v: Any) { } + interface I { + } + object O{} + ``` + +Rule id: `no-empty-class-body` + +## No leading empty lines in method blocks + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun bar() { + val a = 2 + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun bar() { + + val a = 2 + } + ``` + +Rule id: `no-empty-first-line-in-method-block` + +## No line break after else + +Disallows line breaks after the else keyword if that could lead to confusion, for example: + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun funA() { + if (conditionA()) { + doSomething() + } else if (conditionB()) { + doAnotherThing() + } + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun funA() { + if (conditionA()) { + doSomething() + } else + if (conditionB()) { + doAnotherThing() + } + } + ``` + +Rule id: `no-line-break-after-else` + +## No line break before assignment + +When a line is broken at an assignment (`=`) operator the break comes after the symbol. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val valA = + "" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val valA + = "" + ``` + +Rule id: `no-line-break-before-assignment` + +## No multi spaces + +Except in indentation and in KDoc's it is not allowed to have multiple consecutive spaces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + x(1, 3) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + x(1, 3) + } + ``` + +Rule id: `no-multi-spaces` + +## No semicolons + +No semicolons (unless used to separate multiple statements on the same line). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() { + bar() + + bar() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo() { + ; + bar() + ; + + bar() + + ; + } + ``` + +Rule id: `no-semi` + +## No trailing whitespaces + +Rule id: `no-trailing-spaces` + +## No `Unit` as return type + +The `Unit` type is not allowed as return type of a function. +returns (`fun fn {}` instead of `fun fn: Unit {}`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun fn() {} + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun fn(): Unit {} + ``` + +Rule id: `no-unit-return` + +## No unused imports + +!!! warning + This rule is not able to detect *all* unused imports as mentioned in this [issue comment](https://github.com/pinterest/ktlint/issues/1754#issuecomment-1368201667). + +Rule id: `no-unused-imports` + +## No wildcard imports + +No wildcard imports except imports listed in `.editorconfig` property `ij_kotlin_packages_to_use_import_on_demand`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + import foobar.Bar + import foobar.Foo + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + import foobar.* + ``` + +!!! warning + In case property `ij_kotlin_packages_to_use_import_on_demand` is not explicitly set, it allows wildcards imports like `java.util.*` by default to keep in sync with IntelliJ IDEA behavior. To disallow *all* wildcard imports, add property below to your `.editorconfig`: + ```editorconfig + [*.{kt,kts}] + ij_kotlin_packages_to_use_import_on_demand = unset + ``` + +Rule id: `no-wildcard-imports` + +## Package name + +Validates that the package name matches the regular expression `[a-z][a-zA-Z\d]*(\.[a-z][a-zA-Z\d]*)*`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package foo + package foo.bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package Foo + package foo.Foo + package `foo bar` + package foo.`foo bar` + ``` + +Rule id: `package-name` + +## Parameter list wrapping + +When class/function signature doesn't fit on a single line, each parameter must be on a separate line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class ClassA(paramA: String, paramB: String, paramC: String) + class ClassA( + paramA: String, + paramB: String, + paramC: String + ) + fun f(a: Any, b: Any, c: Any) + fun f( + a: Any, + b: Any, + c: Any + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class ClassA( + paramA: String, paramB: String, + paramC: String + ) + fun f( + a: Any, + b: Any, c: Any + ) + ``` + +Rule id: `parameter-list-wrapping` + +## Parameter wrapping + +When a function or class parameter doesn't fit on a single line, wrap the type or value to a separate line + +=== "[:material-heart:](#) Ktlint (ktlint_official)" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: + Foo, + ) + fun bar( + fooooooooooooooooooooooooTooLong: + Foo, + ) + ``` +=== "[:material-heart:](#) Ktlint (non ktlint_official)" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: + Foo, + ) + fun bar( + fooooooooooooooooooooooooTooLong: + Foo, + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: Foo, + ) + fun bar( + fooooooooooooooooooooooooooooTooLong: Foo, + ) + ``` + +Rule id: `parameter-wrapping` + +## Property wrapping + +When a property doesn't fit on a single line, wrap the type or value to a separate line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val aVariableWithALooooooooooooongName: + String + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val aVariableWithALooooooooooooongName: String + ``` + +Rule id: `property-wrapping` + +## String template + +Consistent string templates (`$v` instead of `${v}`, `${p.v}` instead of `${p.v.toString()}`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = "$foo hello" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = "${foo} hello" + ``` + +Rule id: `string-template` + +## Trailing comma on call site + +Consistent removal (default) or adding of trailing commas on call site. + +!!! important + KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma_on_call_site` to configure the rule. When this property is enabled, KtLint *enforces* the usage of the trailing comma at call site while IntelliJ IDEA default formatter only *allows* to use the trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + FooWrapper( + Foo( + a = 3, + b = 4, + ), + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + FooWrapper(Foo( + a = 3, + b = 4, + ),) // it's weird to insert "," between unwrapped (continued) parenthesis + ``` + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on call site has been changed to `true` except when codestyle `android` is used. + + Although the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) leaves it to the developer's discretion to use trailing commas on the call site, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +!!! note + Trailing comma on call site is automatically disabled if the [Wrapping](#wrapping) rule (or, before version `0.45.0`, the [Indentation](#indentation) rule) is disabled or not loaded. Because it cannot provide proper formatting with unwrapped calls. (see [dependencies](./dependencies.md)). + +Rule id: `trailing-comma-on-call-site` + +## Trailing comma on declaration site + +Consistent removal (default) or adding of trailing commas on declaration site. + +!!! important + KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma` to configure the rule. When this property is enabled, KtLint *enforces* the usage of the trailing comma at declaration site while IntelliJ IDEA default formatter only *allows* to use the trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class FooWrapper( + val foo = Foo( + a = 3, + b = 4, + ), + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class FooWrapper(val foo = Foo( + a = 3, + b = 4, + ),) // it's weird to insert "," between unwrapped (continued) parenthesis + ``` + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on declaration site has been changed to `true` except when codestyle `android` is used. + + The [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) encourages the usage of trailing commas on the declaration site, but leaves it to the developer's discretion to use trailing commas on the call site. But next to this, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +!!! note + Trailing comma on declaration site is automatically disabled if the [Wrapping](#wrapping) rule (or, before version `0.45.0`, the [Indentation](#indentation) rule) is disabled or not loaded. Because it cannot provide proper formatting with unwrapped declarations. (see [dependencies](./dependencies.md)). + +Rule id: `trailing-comma-on-declaration-site` + +## Unnecessary parenthesis before trailing lambda + +An empty parentheses block before a lambda is redundant. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + "some-string".count { it == '-' } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + "some-string".count() { it == '-' } + ``` + +Rule id: `unnecessary-parentheses-before-trailing-lambda` + +## Wrapping + +### Wrapping + +Inserts missing newlines (for example between parentheses of a multi-line function call). + +Rule id: `wrapping` + +### Comment wrapping + +A block comment should start and end on a line that does not contain any other element. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* Some comment 1 */ + val foo1 = "foo1" + val foo2 = "foo" // Some comment + val foo3 = { /* no-op */ } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment 1 */ val foo1 = "foo1" + val foo2 = "foo" /* Block comment instead of end-of-line comment */ + val foo3 = "foo" /* Some comment + * with a newline + */ + ``` + +Rule id: `comment-wrapping` + +## Spacing + +### Angle bracket spacing + +No spaces around angle brackets when used for typing. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val a: Map = mapOf() + val b: Map = mapOf() + val c: Map = mapOf() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val a: Map< Int, String> = mapOf() + val b: Map = mapOf() + val c: Map = mapOf() + ``` + +Rule id: `spacing-around-angle-brackets` + +### Annotation spacing + +Annotations should be separated by a single line break. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + @JvmField + fun foo() {} + + /** + * block comment + */ + @Foo @Bar + class FooBar { + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + @JvmField + + fun foo() {} + + @Foo @Bar + /** + * block comment + */ + class FooBar { + } + ``` + +Rule id: `annotation-spacing` + +### Blank line between declarations with annotations + +Declarations with annotations should be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun a() + + @Bar + fun b() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun a() + @Bar + fun b() + ``` + +Rule id: `spacing-between-declarations-with-annotations` + +### Blank line between declaration with comments + +Declarations with comments should be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // some comment 1 + bar() + + /* + * some comment 2 + */ + foo() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // some comment 1 + bar() + /* + * some comment 2 + */ + foo() + ``` + +Rule id: `spacing-between-declarations-with-comments` + +### Colon spacing + +Consistent spacing around colon. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class A : B + class A2 : B2 + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class A:B + class A2 : B2 + ``` + +Rule id: `colon-spacing` + +### Comma spacing + +Consistent spacing around comma. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = Foo(1, 3) + val foo2 = Foo(1, 3) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = Foo(1 ,3) + val foo2 = Foo(1,3) + ``` + +Rule id: `comma-spacing` + +### Comment spacing + +The end of line comment sign `//` should be preceded and followed by exactly a space. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // comment + var debugging = false // comment + var debugging = false // comment + var debugging = false // comment + fun main() { + System.out.println( // 123 + "test" + ) + } + // comment + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + //comment + var debugging = false// comment + var debugging = false //comment + var debugging = false//comment + fun main() { + System.out.println(//123 + "test" + ) + } + //comment + ``` + +Rule id: `comment-spacing` + +### Curly spacing + +Consistent spacing around curly braces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = if (true) { 0 } else { 1 } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = if (true){0}else{1} + ``` + +Rule id: `curly-spacing` + +### Dot spacing + +Consistent spacing around dots. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun String.foo() = "foo" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun String . foo() = "foo" + ``` + +Rule id: `dot-spacing` + +### Double colon spacing + +No spaces around `::`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = Foo::class + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = Foo ::class + val foo2 = Foo:: class + val foo3 = Foo :: class + val foo4 = Foo:: + class + ``` + +Rule id: `double-colon-spacing` + +### Function return type spacing + +Consistent spacing around the function return type. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(): String = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1() : String = "some-result" + fun foo2(): String = "some-result" + fun foo3():String = "some-result" + fun foo4(): + String = "some-result" + ``` + +Rule id: `function-return-type-spacing` + +### Function start of body spacing + +Consistent spacing before start of function body. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1() = "some-result" + fun foo2() = + "some-result" + fun foo3() { + // do something + } + fun bar1(): String = "some-result" + fun bar2(): String = + "some-result" + fun bar3(): String { + return "some-result" + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1()= "some-result" + fun foo2() + = "some-result" + fun foo3() + { + // do something + } + fun bar1(): String= "some-result" + fun bar2(): String + = "some-result" + fun bar3(): String + { + return "some-result" + } + ``` + +Rule id: `function-start-of-body-spacing`: + +### Function type reference spacing + +Consistent spacing in the type reference before a function. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun String.foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun String .foo() = "some-result" + fun String + .foo() = "some-result" + fun String? .foo() = "some-result" + fun String? + .foo() = "some-result" + ``` + +Rule id: `function-type-reference-spacing` + +### Fun keyword spacing + +Consistent spacing after the fun keyword. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo() = "some-result" + fun + foo() = "some-result" + ``` + +Rule id: `fun-keyword-spacing` + +### Kdoc wrapping + +A KDoc comment should start and end on a line that does not contain any other element. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /** Some KDoc comment 1 */ + val foo1 = "foo1" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /** Some KDoc comment 1 */ val foo1 = "foo1" + val foo2 = "foo2" /** Some KDoc comment + * with a newline + */ + ``` + +Rule id: `kdoc-wrapping` + +### Keyword spacing + +Consistent spacing around keywords. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + if (true) {} + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + if(true){} + } + ``` + +Rule id: `keyword-spacing` + +### Modifier list spacing + +Consistent spacing between modifiers in and after the last modifier in a modifier list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + abstract class Foo { + protected abstract suspend fun execute() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + abstract class Foo { + protected abstract suspend fun execute() + } + abstract + class Foo { + protected + abstract + suspend + fun execute() + } + ``` + +Rule id: `modifier-list-spacing` + +### Nullable type spacing + +No spaces in a nullable type. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo: String? = null + val foo: List = listOf(null) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo: String ? = null + val foo: List = listOf(null) + ``` + +Rule id: `nullable-type-spacing` + +### Operator spacing + +Consistent spacing around operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = 1 + 2 + val foo2 = 1 - 2 + val foo3 = 1 * 2 + val foo4 = 1 / 2 + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = 1+2 + val foo2 = 1- 2 + val foo3 = 1 *2 + val foo4 = 1 / 2 + ``` + +Rule id: `op-spacing` + +### Parenthesis spacing + +Consistent spacing around parenthesis. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo : Bar { + constructor(string: String) : super() + } + val foo1 = ((1 + 2) / 3) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class Foo : Bar { + constructor(string: String) : super () + } + val foo1 = ( (1 + 2 ) / 3) + ``` + +Rule id: `paren-spacing` + +### Range spacing + +Consistent spacing around range operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = (1..12 step 2).last + val foo2 = (1..12 step 2).last + val foo3 = (1..12 step 2).last + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = (1.. 12 step 2).last + val foo2 = (1 .. 12 step 2).last + val foo3 = (1 ..12 step 2).last + ``` + +Rule id: `range-spacing` + +### Spacing between function name and opening parenthesis + +Consistent spacing between function name and opening parenthesis. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = "foo" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo () = "foo" + ``` + +Rule id: `spacing-between-function-name-and-opening-parenthesis` + +### Unary operator spacing + +No spaces around unary operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1(i: Int) = i++ + fun foo2(i: Int) = ++i + fun foo3(i: Int) = ++i + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1(i: Int) = i ++ + fun foo2(i: Int) = ++ i + fun foo3(i: Int) = ++ + i + ``` + +Rule id: `unary-op-spacing` diff --git a/documentation/snapshot/mkdocs.yml b/documentation/snapshot/mkdocs.yml new file mode 100644 index 0000000000..ee9f9843b0 --- /dev/null +++ b/documentation/snapshot/mkdocs.yml @@ -0,0 +1,90 @@ +site_name: Ktlint +site_url: https://pinterest.github.io/ktlint/ + +site_dir: build/site +docs_dir: docs + +extra: + version: + provider: mike + +theme: + name: material + custom_dir: overrides + favicon: assets/images/favicon.ico + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: pink + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: pink + toggle: + icon: material/brightness-4 + name: Switch to light mode + icon: + repo: material/github + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - search.suggest + - search.share + +nav: + - Home: index.md + - Installation: + - Overview: install/overview.md + - Command line: install/cli.md + - Integrations: install/integrations.md +# - API: install/api.md TODO: properly document + - Snapshot build: install/snapshot-build.md + - Rules: + - Code styles: rules/code-styles.md + - Standard rules: rules/standard.md + - Experimental rules: rules/experimental.md + - Dependencies: rules/dependencies.md + - KtLint configuration: rules/configuration-ktlint.md + - IntelliJ IDEA configuration: rules/configuration-intellij-idea.md + - API: + - Overview: api/overview.md + - Custom integration: api/custom-integration.md + - Custom rule set: api/custom-rule-set.md + - Custom reporter: api/custom-reporter.md + - Badge: api/badge.md + - FAQ: faq.md + - Contributing: + - Overview: contributing/overview.md + - Guidelines: contributing/guidelines.md + - Code of conduct: contributing/code-of-conduct.md + +plugins: + - search + +repo_url: https://github.com/pinterest/ktlint +repo_name: pinterest/ktlint + +markdown_extensions: + - toc: + permalink: true + - admonition + - pymdownx.emoji: + emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:materialx.emoji.twemoji + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true diff --git a/documentation/snapshot/overrides/main.html b/documentation/snapshot/overrides/main.html new file mode 100644 index 0000000000..e658abc65b --- /dev/null +++ b/documentation/snapshot/overrides/main.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block outdated %} +You're viewing the SNAPSHOT version of the documentation. Be sure to use the + + SNAPSHOT version of ktlint + +, or go to the latest + + RELEASE version of the documentation + +{% endblock %} diff --git a/documentation/snapshot/serve-docs-locally.sh b/documentation/snapshot/serve-docs-locally.sh new file mode 100755 index 0000000000..f4f4522d7b --- /dev/null +++ b/documentation/snapshot/serve-docs-locally.sh @@ -0,0 +1,12 @@ +# !/bin/bash + +echo "Serving docs from directory '$(basename "${PWD##*/}")'" +echo "" + +mkdocs serve +if [[ $? -ne 0 ]]; then + echo "Invalid command. Please ensure that 'mkdocs' is installed." + echo " - If needed install python3" + echo " - If needed run 'pip install mkdocs'" + echo "Or run 'docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material'" +fi diff --git a/gradle.properties b/gradle.properties index 1abf786e19..56bca06be2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.49.1-SNAPSHOT +VERSION_NAME=0.49.2-SNAPSHOT GROUP=com.pinterest.ktlint POM_DESCRIPTION=An anti-bikeshedding Kotlin linter with built-in formatter. @@ -20,4 +20,6 @@ POM_DEVELOPER_NAME=Pinterest, Inc. org.gradle.jvmargs=-Xmx4g org.gradle.parallel=true org.gradle.caching=true +# The release process of ktlint breaks whenever the configuration cache is enabled as not all gradle tasks fully supports this feature yet. +# As the configuration cache only slightly improves the build performance, it is kept disabled for now. org.gradle.configuration-cache=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dbb750b4b..953dd97532 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ kotlinDev = "1.8.20" kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } checksum = "org.gradle.crypto.checksum:1.4.0" shadow = "com.github.johnrengelman.shadow:8.1.1" +sdkman = "io.sdkman.vendors:3.0.0" [libraries] kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } diff --git a/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt b/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt index fda8dd30a8..e97970362b 100644 --- a/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt +++ b/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt @@ -12,7 +12,7 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES_EXECUTION_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution import com.pinterest.ktlint.rule.engine.core.api.editorconfig.createRuleExecutionEditorConfigProperty -import com.pinterest.ktlint.rule.engine.internal.toPropertyBuilderWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue import com.pinterest.ktlint.ruleset.standard.rules.FilenameRule import com.pinterest.ktlint.ruleset.standard.rules.IndentationRule import com.pinterest.ktlint.test.KtlintTestFileSystem diff --git a/ktlint-baseline/build.gradle.kts b/ktlint-baseline/build.gradle.kts deleted file mode 100644 index 2c35a73205..0000000000 --- a/ktlint-baseline/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("ktlint-publication-library") -} - -dependencies { - implementation(projects.ktlintLogger) - implementation(projects.ktlintRuleEngineCore) - implementation(projects.ktlintCliReporterCore) -} diff --git a/ktlint-baseline/gradle.properties b/ktlint-baseline/gradle.properties deleted file mode 100644 index d31c7c9f55..0000000000 --- a/ktlint-baseline/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -POM_NAME=ktlint-baseline -POM_ARTIFACT_ID=ktlint-baseline diff --git a/ktlint-cli-reporter-baseline/build.gradle.kts b/ktlint-cli-reporter-baseline/build.gradle.kts index 589761a8c4..9323b9374e 100644 --- a/ktlint-cli-reporter-baseline/build.gradle.kts +++ b/ktlint-cli-reporter-baseline/build.gradle.kts @@ -3,6 +3,8 @@ plugins { } dependencies { + implementation(projects.ktlintLogger) + implementation(projects.ktlintRuleEngineCore) implementation(projects.ktlintCliReporterCore) testImplementation(projects.ktlintTest) diff --git a/ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt similarity index 96% rename from ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt rename to ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt index 0b49bef889..22d5349200 100644 --- a/ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt +++ b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt @@ -1,8 +1,8 @@ -package com.pinterest.ktlint.cli.api +package com.pinterest.ktlint.cli.reporter.baseline -import com.pinterest.ktlint.cli.api.Baseline.Status.INVALID -import com.pinterest.ktlint.cli.api.Baseline.Status.NOT_FOUND -import com.pinterest.ktlint.cli.api.Baseline.Status.VALID +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.INVALID +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.NOT_FOUND +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.VALID import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.BASELINE_IGNORED import com.pinterest.ktlint.logger.api.initKtLintKLogger diff --git a/ktlint-cli/build.gradle.kts b/ktlint-cli/build.gradle.kts index c253fb1da9..3afaf35942 100644 --- a/ktlint-cli/build.gradle.kts +++ b/ktlint-cli/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("ktlint-publication-library") alias(libs.plugins.shadow) alias(libs.plugins.checksum) + alias(libs.plugins.sdkman) signing } @@ -19,7 +20,6 @@ tasks.shadowJar { } dependencies { - implementation(projects.ktlintBaseline) implementation(projects.ktlintCore) implementation(projects.ktlintLogger) implementation(projects.ktlintCliReporterBaseline) @@ -140,3 +140,11 @@ tasks.withType().configureEach { systemProperty("ktlint-version", ktlintVersion) } } + +sdkman { + val sdkmanVersion = providers.environmentVariable("SDKMAN_VERSION").orElse(project.version.toString()) + candidate.set("ktlint") + version.set(sdkmanVersion) + url.set("https://github.com/pinterest/ktlint/releases/download/$sdkmanVersion/ktlint-$sdkmanVersion.zip") + hashtag.set("ktlint") +} diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt index d90bcb3aee..62506a50fb 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt @@ -14,7 +14,6 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.Deque import java.util.LinkedList -import kotlin.io.path.Path import kotlin.io.path.absolutePathString import kotlin.io.path.isDirectory import kotlin.io.path.pathString @@ -23,7 +22,7 @@ import kotlin.system.measureTimeMillis private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() -private val WORK_DIR: String = File(".").canonicalPath +private val ROOT_DIR_PATH: Path = Paths.get("").toAbsolutePath() private val TILDE_REGEX = Regex("^(!)?~") private const val NEGATION_PREFIX = "!" @@ -66,7 +65,7 @@ internal fun FileSystem.fileSequence( .filter { it.startsWith(NEGATION_PREFIX) } .map { getPathMatcher(it.removePrefix(NEGATION_PREFIX)) } - val pathMatchers = + val includeGlobs = globs .filterNot { it.startsWith(NEGATION_PREFIX) } .let { includeMatchers -> @@ -81,23 +80,27 @@ internal fun FileSystem.fileSequence( } else { includeMatchers } - }.map { getPathMatcher(it) } - - LOGGER.debug { - """ - Start walkFileTree for rootDir: '$rootDir' - include: - ${pathMatchers.map { - " - $it" - }} - exclude: - ${negatedPathMatchers.map { " - $it" }} - """.trimIndent() + } + var commonRootDir = rootDir + patterns.forEach { pattern -> + try { + val patternDir = + rootDir + .resolve(pattern) + .normalize() + commonRootDir = commonRootDir.findCommonParentDir(patternDir) + } catch (e: InvalidPathException) { + // Windows throws an exception when you pass a glob to Path#resolve. + } } + + val pathMatchers = includeGlobs.map { getPathMatcher(it) } + + LOGGER.debug { "Start walkFileTree from directory: '$commonRootDir'" } val duration = measureTimeMillis { Files.walkFileTree( - rootDir, + commonRootDir, object : SimpleFileVisitor() { override fun visitFile( filePath: Path, @@ -148,6 +151,16 @@ internal fun FileSystem.fileSequence( return result.asSequence() } +private fun Path.findCommonParentDir(path: Path): Path = + when { + path.startsWith(this) -> + this + startsWith(path) -> + path + else -> + this@findCommonParentDir.findCommonParentDir(path.parent) + } + private fun FileSystem.expand( patterns: List, rootDir: Path, @@ -334,9 +347,18 @@ private val onWindowsOS .getProperty("os.name") .startsWith("windows", true) +/** + * Gets the relative route of the path. Also adjusts the slashes for uniformity between file systems. + */ internal fun File.location(relative: Boolean) = if (relative) { - this.toPath().relativeToOrSelf(Path(WORK_DIR)).pathString + this + .toPath() + .relativeToOrSelf(ROOT_DIR_PATH) + .pathString + .replace(File.separatorChar, '/') } else { - this.path + this + .path + .replace(File.separatorChar, '/') } diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 7307c5510e..e2aaac047d 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -2,9 +2,9 @@ package com.pinterest.ktlint.cli.internal import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger -import com.pinterest.ktlint.cli.api.Baseline -import com.pinterest.ktlint.cli.api.doesNotContain -import com.pinterest.ktlint.cli.api.loadBaseline +import com.pinterest.ktlint.cli.reporter.baseline.Baseline +import com.pinterest.ktlint.cli.reporter.baseline.doesNotContain +import com.pinterest.ktlint.cli.reporter.baseline.loadBaseline import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.FORMAT_IS_AUTOCORRECTED import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.KOTLIN_PARSE_EXCEPTION @@ -51,8 +51,6 @@ import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -import kotlin.collections.ArrayList -import kotlin.collections.LinkedHashSet import kotlin.concurrent.thread import kotlin.io.path.pathString import kotlin.io.path.relativeToOrSelf @@ -263,9 +261,9 @@ internal class KtlintCommandLine { }.applyIf(disabledRules.isNotBlank()) { logger.debug { "Add editor config override to disable rules: '$disabledRules'" } plus(*disabledRulesEditorConfigOverrides()) - }.applyIf(android) { - logger.debug { "Add editor config override to set code style to 'android'" } - plus(CODE_STYLE_PROPERTY to CodeStyleValue.android) + }.applyIf(android || codeStyle == CodeStyleValue.android || codeStyle == CodeStyleValue.android_studio) { + logger.debug { "Add editor config override to set code style to 'android_studio'" } + plus(CODE_STYLE_PROPERTY to CodeStyleValue.android_studio) }.applyIf(stdin) { logger.debug { "Add editor config override to disable 'filename' rule which can not be used in combination with reading from " + @@ -409,16 +407,21 @@ internal class KtlintCommandLine { .map { it.toFile() } .takeWhile { errorNumber.get() < limit } .map { file -> - val fileName = file.toPath().relativeRoute Callable { - fileName to + file to process( ktLintRuleEngine = ktLintRuleEngine, code = Code.fromFile(file), - baselineLintErrors = lintErrorsPerFile.getOrDefault(fileName, emptyList()), + baselineLintErrors = + lintErrorsPerFile + .getOrDefault( + // Baseline stores the lint violations as relative path to work dir + file.location(true), + emptyList(), + ), ) } - }.parallel({ (fileName, errList) -> report(Paths.get(fileName).relativeRoute, errList, reporter) }) + }.parallel({ (file, errList) -> report(file.location(relative), errList, reporter) }) } private fun lintStdin( @@ -734,6 +737,15 @@ internal fun List.toFilesURIList() = jarFile.toURI().toURL() } +/** + * Wrapper around exitProcess which ensure that a proper log line is written which can be used in unit tests for + * validating the result of the test. + */ +internal fun exitKtLintProcess(status: Int): Nothing { + logger.debug { "Exit ktlint with exit code: $status" } + exitProcess(status) +} + /** * Gets the relative route of the path. Also adjusts the slashes for uniformity between file systems. */ @@ -745,12 +757,3 @@ internal val Path.relativeRoute: String .pathString .replace(File.separatorChar, '/') } - -/** - * Wrapper around exitProcess which ensure that a proper log line is written which can be used in unit tests for - * validating the result of the test. - */ -internal fun exitKtLintProcess(status: Int): Nothing { - logger.debug { "Exit ktlint with exit code: $status" } - exitProcess(status) -} diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt index 246c8a223b..cb9cb5d7ee 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt @@ -1,8 +1,8 @@ package com.pinterest.ktlint.cli.internal -import com.pinterest.ktlint.cli.api.Baseline import com.pinterest.ktlint.cli.internal.ReporterAggregator.ReporterConfigurationElement.ARTIFACT import com.pinterest.ktlint.cli.internal.ReporterAggregator.ReporterConfigurationElement.OUTPUT +import com.pinterest.ktlint.cli.reporter.baseline.Baseline import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.ReporterProviderV2 import com.pinterest.ktlint.cli.reporter.core.api.ReporterV2 diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt index 54cec00d40..9e69ce8983 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt @@ -476,4 +476,39 @@ class SimpleCLITest { .doesNotContainLineMatching(Regex(".*Needless blank line.*")) } } + + @Nested + inner class `Issue 1983 - Enable android code style` { + @Test + fun `Enable android code style via parameter --android`( + @TempDir + tempDir: Path, + ) { + CommandLineTestRunner(tempDir) + .run( + testProjectName = "too-many-empty-lines", + arguments = listOf("--android"), + ) { + assertThat(normalOutput).containsLineMatching( + Regex(".*Add editor config override to set code style to 'android_studio'.*"), + ) + } + } + + @Test + fun `Enable android code style via parameter --code-style=android_studio`( + @TempDir + tempDir: Path, + ) { + CommandLineTestRunner(tempDir) + .run( + testProjectName = "too-many-empty-lines", + arguments = listOf("--code-style=android_studio"), + ) { + assertThat(normalOutput).containsLineMatching( + Regex(".*Add editor config override to set code style to 'android_studio'.*"), + ) + } + } + } } diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt index a189ad66cc..4d4f6bce22 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt @@ -15,9 +15,10 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "TestBaselineFile.kt.test", "some/path/to/TestBaselineFile2.kt.test", @@ -26,10 +27,10 @@ class BaselineCLITest { SoftAssertions().apply { assertErrorExitCode() assertThat(normalOutput) - .containsLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .containsLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineFile2.kt.test:1:25: Unnecessary block.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineFile2.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/some/path/to/TestBaselineFile2.kt.test:1:25: Unnecessary block.*")) + .containsLineMatching(Regex(".*/$projectName/some/path/to/TestBaselineFile2.kt.test:2:1: Unexpected blank line.*")) }.assertAll() } } @@ -41,11 +42,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineFile.kt.test", @@ -55,10 +57,14 @@ class BaselineCLITest { SoftAssertions().apply { assertNormalExitCode() assertThat(normalOutput) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*"), + ) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + @@ -75,11 +81,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "config/test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineFile.kt.test", @@ -89,10 +96,14 @@ class BaselineCLITest { SoftAssertions().apply { assertNormalExitCode() assertThat(normalOutput) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*"), + ) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + @@ -145,11 +156,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineExtraErrorFile.kt.test", @@ -159,8 +171,10 @@ class BaselineCLITest { SoftAssertions().apply { assertErrorExitCode() assertThat(normalOutput) - .containsLineMatching(Regex("^TestBaselineExtraErrorFile.kt.test:2:1: Unexpected blank line.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineExtraErrorFile2.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineExtraErrorFile.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineExtraErrorFile2.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt index cfebaa39a9..f3e56b49c2 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt @@ -13,6 +13,7 @@ import org.junit.jupiter.api.condition.OS import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import java.nio.file.Path +import kotlin.io.path.absolutePathString private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() @@ -35,6 +36,7 @@ internal class FileUtilsTest { private val ktFile2InProjectSubDirectory = "project1/src/main/kotlin/example/Two.kt" private val ktsFileInProjectSubDirectory = "project1/src/scripts/Script.kts" private val javaFileInProjectSubDirectory = "project1/src/main/java/One.java" + private val someFileInOtherProjectRootDirectory = "other-project/SomeFile.txt" @BeforeEach internal fun setUp() { @@ -52,6 +54,7 @@ internal class FileUtilsTest { createFile(ktFile1InProjectSubDirectory) createFile(ktFile2InProjectSubDirectory) createFile(javaFileInProjectSubDirectory) + createFile(someFileInOtherProjectRootDirectory) } } @@ -414,6 +417,68 @@ internal class FileUtilsTest { ) } + @DisabledOnOs(OS.WINDOWS) + @Test + fun `Issue 2002 - On non-Windows OS, find files in a sibling directory based on a relative path to the working directory`() { + val foundFiles = + getFiles( + patterns = listOf("../project1"), + rootDir = ktlintTestFileSystem.resolve(someFileInOtherProjectRootDirectory).parent.toAbsolutePath(), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktsFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ktsFileInProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ktsFileRootDirectory, + ) + } + + @DisabledOnOs(OS.WINDOWS) + @Test + fun `Issue 2002 - On non-Windows OS, find files in a sibling directory based on a relative glob`() { + val foundFiles = + getFiles( + patterns = listOf("../project1/**/*.kt"), + rootDir = ktlintTestFileSystem.resolve("other-project"), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ) + } + + @Test + fun `Issue 2002 - Find files in a sibling directory based on an absolute path`() { + val foundFiles = + getFiles( + patterns = listOf(ktlintTestFileSystem.resolve(ktFileInProjectRootDirectory).parent.absolutePathString()), + rootDir = ktlintTestFileSystem.resolve(someFileInOtherProjectRootDirectory).parent.toAbsolutePath(), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktsFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ktsFileInProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ktsFileRootDirectory, + ) + } + private fun KtlintTestFileSystem.createFile(fileName: String) = writeFile( relativeDirectoryToRoot = fileName.substringBeforeLast("/", ""), diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt new file mode 100644 index 0000000000..c918465173 --- /dev/null +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt @@ -0,0 +1,36 @@ +package com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j + +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import org.ec4j.core.model.Property +import org.ec4j.core.model.PropertyType + +/** + * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. + */ +public fun EditorConfigProperty.toPropertyBuilderWithValue(value: String): Property.Builder = + Property + .builder() + .type(type) + .name(name) + .value(value) + +/** + * Creates an [org.ec4j.core.model.Property] for given [value]. + */ +public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = toPropertyBuilderWithValue(value).build() + +/** + * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. + */ +public fun EditorConfigProperty.toPropertyBuilderWithValue(value: PropertyType.PropertyValue<*>): Property.Builder = + Property + .builder() + .type(type) + .name(name) + .value(value) + +/** + * Creates an [org.ec4j.core.model.Property] for given [value]. + */ +public fun EditorConfigProperty.toPropertyWithValue(value: PropertyType.PropertyValue<*>): Property = + toPropertyBuilderWithValue(value).build() diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt index 3b3f77ccc6..af32e74763 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt @@ -1,7 +1,7 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.test.KtlintTestFileSystem import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatNoException diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt index 7e7c533e97..a393c46efd 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt @@ -1,6 +1,6 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt index e4a56d3925..a190c70dd4 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt @@ -1,6 +1,6 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt index b79ba04e1e..7957998ee8 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt @@ -13,6 +13,7 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAGS_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_OFF_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_ON_ENABLED_PROPERTY diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt index 6eac4c9ae3..648fbea38b 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt @@ -3,34 +3,34 @@ package com.pinterest.ktlint.rule.engine.internal import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import org.ec4j.core.model.Property import org.ec4j.core.model.PropertyType +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue as ec4jToPropertyBuilderWithPropertyValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue as ec4jToPropertyBuilderWithStringValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue as ec4jToPropertyWithPropertyValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue as ec4jToPropertyWithStringValue /** * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyBuilderWithValue(value: String): Property.Builder = - Property - .builder() - .type(type) - .name(name) - .value(value) + ec4jToPropertyBuilderWithStringValue(value) /** * Creates an [org.ec4j.core.model.Property] for given [value]. */ -public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = toPropertyBuilderWithValue(value).build() +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") +public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = ec4jToPropertyWithStringValue(value) /** * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyBuilderWithValue(value: PropertyType.PropertyValue<*>): Property.Builder = - Property - .builder() - .type(type) - .name(name) - .value(value) + ec4jToPropertyBuilderWithPropertyValue(value) /** * Creates an [org.ec4j.core.model.Property] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyWithValue(value: PropertyType.PropertyValue<*>): Property = - toPropertyBuilderWithValue(value).build() + ec4jToPropertyWithPropertyValue(value) diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt index 90d0708b9a..ca45cac1c6 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt @@ -1,10 +1,5 @@ package com.pinterest.ktlint.rule.engine.internal -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import com.pinterest.ktlint.logger.api.initKtLintKLogger -import com.pinterest.ktlint.logger.api.setDefaultLoggerModifier -import mu.KotlinLogging import org.assertj.core.api.Assertions.assertThat import org.ec4j.core.EditorConfigLoader import org.ec4j.core.Resource @@ -17,16 +12,6 @@ import java.nio.charset.StandardCharsets import java.nio.file.Paths class ThreadSafeEditorConfigCacheTest { - init { - // Overwrite default logging with TRACE logging by initializing *and* printing first log statement before - // loading any other classes. - KotlinLogging - .logger {} - .setDefaultLoggerModifier { logger -> (logger.underlyingLogger as Logger).level = Level.TRACE } - .initKtLintKLogger() - .trace { "Enable trace logging for unit test" } - } - @Test fun `Given a file which is requested multiple times then it is read only once and then stored into and retrieved from the cache`() { val threadSafeEditorConfigCache = ThreadSafeEditorConfigCache() diff --git a/ktlint-ruleset-standard/build.gradle.kts b/ktlint-ruleset-standard/build.gradle.kts index 155380a27a..e97bbf2a54 100644 --- a/ktlint-ruleset-standard/build.gradle.kts +++ b/ktlint-ruleset-standard/build.gradle.kts @@ -4,7 +4,6 @@ plugins { dependencies { implementation(projects.ktlintLogger) - implementation(libs.logging) api(projects.ktlintCliRulesetCore) api(projects.ktlintRuleEngineCore) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 6496a9da29..68f5df6817 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -39,12 +39,14 @@ import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLinesInChainedMethodCa import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveBlankLinesRule import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveCommentsRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyClassBodyRule +import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInClassBodyRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInMethodBlockRule import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakAfterElseRule import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakBeforeAssignmentRule import com.pinterest.ktlint.ruleset.standard.rules.NoMultipleSpacesRule import com.pinterest.ktlint.ruleset.standard.rules.NoSemicolonsRule +import com.pinterest.ktlint.ruleset.standard.rules.NoSingleLineBlockCommentRule import com.pinterest.ktlint.ruleset.standard.rules.NoTrailingSpacesRule import com.pinterest.ktlint.ruleset.standard.rules.NoUnitReturnRule import com.pinterest.ktlint.ruleset.standard.rules.NoUnusedImportsRule @@ -120,12 +122,14 @@ public class StandardRuleSetProvider : RuleProvider { NoConsecutiveBlankLinesRule() }, RuleProvider { NoConsecutiveCommentsRule() }, RuleProvider { NoEmptyClassBodyRule() }, + RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyFirstLineInClassBodyRule() }, RuleProvider { NoEmptyFirstLineInMethodBlockRule() }, RuleProvider { NoLineBreakAfterElseRule() }, RuleProvider { NoLineBreakBeforeAssignmentRule() }, RuleProvider { NoMultipleSpacesRule() }, RuleProvider { NoSemicolonsRule() }, + RuleProvider { NoSingleLineBlockCommentRule() }, RuleProvider { NoTrailingSpacesRule() }, RuleProvider { NoUnitReturnRule() }, RuleProvider { NoUnusedImportsRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt index d08f75668d..24998e9685 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt @@ -1,7 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT -import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE import com.pinterest.ktlint.rule.engine.core.api.RuleId @@ -18,13 +17,10 @@ import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement -import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl import org.jetbrains.kotlin.psi.psiUtil.leaves /** - * Checks external wrapping of block comments. Wrapping inside the comment is not altered. A block comment following another element on the - * same line is replaced with an EOL comment, if possible. + * Checks external wrapping of block comments. Wrapping inside the comment is not altered. */ public class CommentWrappingRule : StandardRule( @@ -54,17 +50,6 @@ public class CommentWrappingRule : .firstOrNull() ?: node.lastChildLeafOrSelf() - if (!node.textContains('\n') && - !node.isKtlintSuppressionDirective() && - beforeBlockComment.prevLeaf().isWhitespaceWithNewlineOrNull() && - afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() - ) { - emit(node.startOffset, "A single line block comment must be replaced with an EOL comment", true) - if (autoCorrect) { - node.replaceWithEndOfLineComment() - } - } - if (!beforeBlockComment.prevLeaf().isWhitespaceWithNewlineOrNull() && !afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() ) { @@ -112,7 +97,6 @@ public class CommentWrappingRule : ) if (autoCorrect) { node.upsertWhitespaceBeforeMe(" ") - node.replaceWithEndOfLineComment() } } } @@ -133,24 +117,7 @@ public class CommentWrappingRule : } } - private fun ASTNode.replaceWithEndOfLineComment() { - val content = text.removeSurrounding("/*", "*/").trim() - val eolComment = PsiCommentImpl(EOL_COMMENT, "// $content") - (this as LeafPsiElement).rawInsertBeforeMe(eolComment) - rawRemove() - } - private fun ASTNode?.isWhitespaceWithNewlineOrNull() = this == null || this.isWhiteSpaceWithNewline() - - // TODO: Remove when ktlint suppression directive in comments are no longer supported - private fun ASTNode?.isKtlintSuppressionDirective() = - this - ?.text - ?.removePrefix("/*") - ?.removeSuffix("*/") - ?.trim() - ?.let { it.startsWith("ktlint-enable") || it.startsWith("ktlint-disable") } - ?: false } public val COMMENT_WRAPPING_RULE_ID: RuleId = CommentWrappingRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt index 35af2368b5..28a6ebc722 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt @@ -717,6 +717,7 @@ public class FunctionSignatureRule : ?.prevCodeLeaf() public companion object { + private const val FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET = Int.MAX_VALUE public val FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = @@ -728,8 +729,22 @@ public class FunctionSignatureRule : PropertyType.PropertyValueParser.POSITIVE_INT_VALUE_PARSER, setOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "unset"), ), - defaultValue = Int.MAX_VALUE, + defaultValue = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET, ktlintOfficialCodeStyleDefaultValue = 2, + propertyMapper = { property, _ -> + if (property?.isUnset == true) { + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET + } else { + property?.getValueAs() + } + }, + propertyWriter = { property -> + if (property == FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET) { + "unset" + } else { + property.toString() + } + }, ) public val FUNCTION_BODY_EXPRESSION_WRAPPING_PROPERTY: EditorConfigProperty = diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt index 81142c414f..39ae35af94 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt @@ -53,7 +53,6 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACKET -import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.REGULAR_STRING_PART import com.pinterest.ktlint.rule.engine.core.api.ElementType.RETURN_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR @@ -89,12 +88,14 @@ import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRu import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isPartOf import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment import com.pinterest.ktlint.rule.engine.core.api.isRoot import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace @@ -451,7 +452,7 @@ public class IndentationRule : } private fun ASTNode.calculateIndentOfFunctionLiteralParameters() = - if (codeStyle == ktlint_official || isFirstParameterOfFunctionLiteralPrecededByNewLine()) { + if (isFirstParameterOfFunctionLiteralPrecededByNewLine()) { // val fieldExample = // LongNameClass { // paramA, @@ -468,13 +469,23 @@ public class IndentationRule : // paramC -> // ClassB(paramA, paramB, paramC) // } - parent(CALL_EXPRESSION) - ?.let { callExpression -> - val textBeforeFirstParameter = - callExpression.findChildByType(REFERENCE_EXPRESSION)?.text + - " { " - " ".repeat(textBeforeFirstParameter.length) - } + // val fieldExample = + // someFunction( + // 1, + // 2, + // ) { paramA, + // paramB, + // paramC -> + // ClassB(paramA, paramB, paramC) + // } + this + .takeIf { it.isPartOf(CALL_EXPRESSION) } + ?.treeParent + ?.leaves(false) + ?.takeWhile { !it.isWhiteSpaceWithNewline() } + ?.sumOf { it.textLength } + ?.plus(2) // need to add spaces to compensate for "{ " + ?.let { length -> " ".repeat(length) } ?: indentConfig.indent.repeat(2) } @@ -1083,7 +1094,7 @@ public class IndentationRule : emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { if (!this::stringTemplateIndenter.isInitialized) { - stringTemplateIndenter = StringTemplateIndenter(indentConfig) + stringTemplateIndenter = StringTemplateIndenter(codeStyle, indentConfig) } stringTemplateIndenter.visitClosingQuotes(currentIndent(), node.treeParent, autoCorrect, emit) } @@ -1091,7 +1102,7 @@ public class IndentationRule : private fun ASTNode?.isElvisOperator() = this != null && elementType == OPERATION_REFERENCE && - firstChildNode.elementType == ELVIS + firstChildNode?.elementType == ELVIS private fun ASTNode.acceptableTrailingSpaces(): String { require(elementType == WHITE_SPACE) @@ -1215,7 +1226,10 @@ private fun String.textWithEscapedTabAndNewline(): String { ).plus(suffix) } -private class StringTemplateIndenter(private val indentConfig: IndentConfig) { +private class StringTemplateIndenter( + private val codeStyle: CodeStyleValue, + private val indentConfig: IndentConfig, +) { fun visitClosingQuotes( expectedIndent: String, node: ASTNode, @@ -1243,7 +1257,7 @@ private class StringTemplateIndenter(private val indentConfig: IndentConfig) { val prefixLength = node.getCommonPrefixLength() val prevLeaf = node.prevLeaf() val correctedExpectedIndent = - if (node.isRawStringLiteralReturnInFunctionBodyBlock()) { + if (codeStyle == ktlint_official && node.isRawStringLiteralReturnInFunctionBodyBlock()) { // Allow: // fun foo(): String { // return """ @@ -1253,7 +1267,7 @@ private class StringTemplateIndenter(private val indentConfig: IndentConfig) { node .indent(false) .plus(indentConfig.indent) - } else if (node.isRawStringLiteralFunctionBodyExpression()) { + } else if (codeStyle == ktlint_official && node.isRawStringLiteralFunctionBodyExpression()) { // Allow: // fun foo( // bar: String diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt index e215349c93..fc871a22c2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt @@ -62,7 +62,7 @@ public class NoConsecutiveBlankLinesRule : StandardRule("no-consecutive-blank-li ?.let { prevNode -> prevNode.elementType == IDENTIFIER && prevNode.treeParent.elementType == CLASS && - this.treeNext.elementType == PRIMARY_CONSTRUCTOR + this.treeNext?.elementType == PRIMARY_CONSTRUCTOR } ?: false } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt new file mode 100644 index 0000000000..49bb8921a3 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -0,0 +1,42 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.children +import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment +import com.pinterest.ktlint.rule.engine.core.api.isRoot +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experimental { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { it.isRoot() } + ?.takeIf { it.isEmptyFile() } + ?.let { emit(0, "File '${node.getFileName()}' should not be empty", false) } + } + + private fun ASTNode.getFileName() = + psi + .containingFile + .virtualFile + .name + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") + + private fun ASTNode.isEmptyFile(): Boolean = + null == + children() + .firstOrNull { + !it.isWhiteSpace() && + !it.isPartOfComment() && + it.elementType != ElementType.PACKAGE_DIRECTIVE && + it.elementType != ElementType.IMPORT_LIST && + !(it.elementType == ElementType.SCRIPT && it.text.isBlank()) + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt new file mode 100644 index 0000000000..08b99363bc --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt @@ -0,0 +1,81 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule +import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf +import com.pinterest.ktlint.rule.engine.core.api.nextLeaf +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl +import org.jetbrains.kotlin.psi.psiUtil.leaves + +/** + * A block comment following another element on the same line is replaced with an EOL comment, if possible. + */ +public class NoSingleLineBlockCommentRule : + StandardRule( + id = "no-single-line-block-comment", + usesEditorConfigProperties = + setOf( + INDENT_SIZE_PROPERTY, + INDENT_STYLE_PROPERTY, + ), + visitorModifiers = setOf(RunAfterRule(COMMENT_WRAPPING_RULE_ID, REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED)), + ), + Rule.Experimental, + Rule.OfficialCodeStyle { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType == BLOCK_COMMENT) { + val afterBlockComment = + node + .leaves() + .takeWhile { it.isWhiteSpace() && !it.textContains('\n') } + .firstOrNull() + ?: node.lastChildLeafOrSelf() + + if (!node.textContains('\n') && + !node.isKtlintSuppressionDirective() && + afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() + ) { + emit(node.startOffset, "Replace the block comment with an EOL comment", true) + if (autoCorrect) { + node.replaceWithEndOfLineComment() + } + } + } + } + + private fun ASTNode.replaceWithEndOfLineComment() { + val content = text.removeSurrounding("/*", "*/").trim() + val eolComment = PsiCommentImpl(EOL_COMMENT, "// $content") + (this as LeafPsiElement).rawInsertBeforeMe(eolComment) + rawRemove() + } + + private fun ASTNode?.isWhitespaceWithNewlineOrNull() = this == null || this.isWhiteSpaceWithNewline() + + // TODO: Remove when ktlint suppression directive in comments are no longer supported + private fun ASTNode?.isKtlintSuppressionDirective() = + this + ?.text + ?.removePrefix("/*") + ?.removeSuffix("*/") + ?.trim() + ?.let { it.startsWith("ktlint-enable") || it.startsWith("ktlint-disable") } + ?: false +} + +public val NO_SINGLE_LINE_BLOCK_COMMENT_RULE_ID: RuleId = NoSingleLineBlockCommentRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt index 840d0c48c1..3f0926b3a1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt @@ -19,8 +19,11 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MAX_LINE_LENGTH_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.indent import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.parent +import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.prevSibling import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe @@ -33,6 +36,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.leaves public class ParameterListWrappingRule : StandardRule( @@ -120,7 +124,9 @@ public class ParameterListWrappingRule : private fun ASTNode.needToWrapParameterList() = when { hasNoParameters() -> false - isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> false + codeStyle != ktlint_official && isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> false + codeStyle == ktlint_official && isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression() -> + false isFunctionTypeWrappedInNullableType() -> false textContains('\n') -> true codeStyle == ktlint_official && containsAnnotatedParameter() -> true @@ -135,7 +141,23 @@ public class ParameterListWrappingRule : private fun ASTNode.isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle(): Boolean { require(elementType == VALUE_PARAMETER_LIST) - return codeStyle != ktlint_official && treeParent?.elementType == FUNCTION_LITERAL + return treeParent?.elementType == FUNCTION_LITERAL + } + + private fun ASTNode.isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression(): Boolean { + require(elementType == VALUE_PARAMETER_LIST) + return firstChildLeafOrSelf() + .let { startOfFunctionLiteral -> + treeParent + ?.takeIf { it.elementType == FUNCTION_LITERAL } + ?.prevCodeLeaf() + ?.takeIf { it.treeParent.elementType == ElementType.VALUE_ARGUMENT_LIST } + ?.takeIf { it.treeParent.treeParent.elementType == ElementType.CALL_EXPRESSION } + ?.leaves() + ?.takeWhile { it != startOfFunctionLiteral } + ?.none { it.isWhiteSpaceWithNewline() } + ?: false + } } private fun ASTNode.isFunctionTypeWrappedInNullableType(): Boolean { diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt index 6ee2f51716..4ca97cf275 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt @@ -1,7 +1,5 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -9,26 +7,6 @@ import org.junit.jupiter.api.Test class CommentWrappingRuleTest { private val commentWrappingRuleAssertThat = assertThatRule { CommentWrappingRule() } - @Test - fun `Given a single line block comment then replace it with an EOL comment`() { - val code = - """ - fun bar() { - /* Some comment */ - } - """.trimIndent() - val formattedCode = - """ - fun bar() { - // Some comment - } - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasLintViolation(2, 5, "A single line block comment must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - @Test fun `Given a multi line block comment that start starts and end on a separate line then do not reformat`() { val code = @@ -107,73 +85,6 @@ class CommentWrappingRuleTest { } } - @Nested - inner class `Given some code code followed by a block comment on the same line` { - @Test - fun `Given a comment followed by a property and separated with space`() { - val code = - """ - val foo = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 17, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a property but not separated with space`() { - val code = - """ - val foo = "foo"/* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 16, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a function and separated with space`() { - val code = - """ - fun foo() = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - fun foo() = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 19, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a function but not separated with space`() { - val code = - """ - fun foo() = "foo"/* Some comment */ - """.trimIndent() - val formattedCode = - """ - fun foo() = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 18, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - } - @Test fun `Given a block comment containing a newline which is preceded by another element on the same line then raise lint error but do not autocorrect`() { val code = @@ -187,22 +98,6 @@ class CommentWrappingRuleTest { .hasLintViolationWithoutAutoCorrect(1, 17, "A block comment after any other element on the same line must be separated by a new line") } - @Test - fun `Given a block comment that does not contain a newline and which is after some code om the same line is changed to an EOL comment`() { - val code = - """ - val foo = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 17, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - @Test fun `Given a block comment in between code elements on the same line then raise lint error but do not autocorrect`() { val code = @@ -246,28 +141,4 @@ class CommentWrappingRuleTest { .hasLintViolation(2, 24, "A block comment may not be followed by any other element on that same line") .isFormattedAs(formattedCode) } - - @Test - fun `Given a single line block containing a block comment then do not reformat`() { - val code = - """ - val foo = { /* no-op */ } - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasNoLintViolations() - } - - @Test - fun `Given single line block comments to disable or enable ktlint then do not reformat`() { - val code = - """ - /* ktlint-disable foo-rule-id bar-rule-id */ - val foo = "foo" - /* ktlint-enable foo-rule-id bar-rule-id */ - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasNoLintViolations() - } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt index 26c5a3703c..661faffdac 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt @@ -3,16 +3,20 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.ruleset.standard.rules.FunctionSignatureRule.Companion.FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY import com.pinterest.ktlint.ruleset.standard.rules.FunctionSignatureRule.Companion.FUNCTION_BODY_EXPRESSION_WRAPPING_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.EOL_CHAR import com.pinterest.ktlint.test.KtLintAssertThat.Companion.MAX_LINE_LENGTH_MARKER import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import com.pinterest.ktlint.test.LintViolation +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.EnumSource class FunctionSignatureRuleTest { @@ -1131,6 +1135,115 @@ class FunctionSignatureRuleTest { } } + @Nested + inner class `Property ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` { + val propertyMapper = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.propertyMapper!! + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a null property then the property mapper returns null`(codeStyleValue: CodeStyleValue) { + val actual = propertyMapper(null, codeStyleValue) + + assertThat(actual).isNull() + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a property which is unset then the property mapper returns max integer which is set as the default value`( + codeStyleValue: CodeStyleValue, + ) { + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue("unset") + + val actual = propertyMapper(property, codeStyleValue) + + assertThat(actual).isEqualTo(Int.MAX_VALUE) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a valid string value then the property mapper returns the integer value`(codeStyleValue: CodeStyleValue) { + val someValue = 123 + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someValue.toString()) + + val actual = propertyMapper(property, codeStyleValue) + + assertThat(actual).isEqualTo(someValue) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a negative value then the property mapper throws and exception`(codeStyleValue: CodeStyleValue) { + val someNegativeValue = "-1" + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someNegativeValue) + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects a " + + "positive integer; found '$someNegativeValue'", + ) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a value bigger than max integer then the property mapper throws and exception`(codeStyleValue: CodeStyleValue) { + val someValueBiggerThanMaxInt = (1L + Int.MAX_VALUE).toString() + val property = + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someValueBiggerThanMaxInt) + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects an " + + "integer. The parsed '$someValueBiggerThanMaxInt' is not an integer.", + ) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a invalid string value then the property mapper returns the integer value`(codeStyleValue: CodeStyleValue) { + val property = + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue("some-invalid-value") + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects an " + + "integer. The parsed 'some-invalid-value' is not an integer.", + ) + } + + @ParameterizedTest(name = "Input value: {0}, output value: {1}") + @CsvSource( + value = [ + "1, 1", + "${Int.MAX_VALUE}, unset", + ], + ) + fun `Given a property with an integer value than write that property`( + inputValue: Int, + expectedOutputValue: String, + ) { + val actual = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.propertyWriter(inputValue) + + assertThat(actual).isEqualTo(expectedOutputValue) + } + } + + @Test + fun `Given the ktlint_official code style then avoid wrapping of parameters by overriding property ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than`() { + val code = + """ + fun f(a: Any, b: Any, c: Any): String { + // body + } + """.trimIndent() + functionSignatureWrappingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .withEditorConfigOverride(FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY to "unset") + .hasNoLintViolations() + } + private companion object { const val UNEXPECTED_SPACES = " " const val NO_SPACE = "" diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index f1c49672ef..5e816c0b2f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -1,6 +1,7 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.intellij_idea import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY @@ -4818,10 +4819,11 @@ internal class IndentationRuleTest { .addAdditionalRuleProvider { ParameterListWrappingRule() } .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationForAdditionalRule(2, 20, "Parameter should start on a newline") - .hasLintViolations( - LintViolation(3, 1, "Unexpected indentation (19) (should be 12)"), - LintViolation(4, 1, "Unexpected indentation (19) (should be 12)"), - ).isFormattedAs(formattedCode) +// .hasLintViolations( +// LintViolation(3, 1, "Unexpected indentation (19) (should be 12)"), +// LintViolation(4, 1, "Unexpected indentation (19) (should be 12)"), +// ) + .isFormattedAs(formattedCode) } @Test @@ -5055,32 +5057,6 @@ internal class IndentationRuleTest { @Nested inner class `Given a function with raw string literal as result` { - @Test - fun `As body expression on same line as equals and preceded by space`() { - val code = - """ - private fun foo( - bar: String, - ) = $MULTILINE_STRING_QUOTE - bar - $MULTILINE_STRING_QUOTE.trimIndent() - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `As body expression on same line as equals but not preceded by space`() { - val code = - """ - private fun foo( - bar: String, - ) =$MULTILINE_STRING_QUOTE - bar - $MULTILINE_STRING_QUOTE.trimIndent() - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() - } - @Test fun `As body expression on next line`() { val code = @@ -5093,35 +5069,165 @@ internal class IndentationRuleTest { indentationRuleAssertThat(code).hasNoLintViolations() } - @Test - fun `As block body`() { - val code = - """ - private fun foo( bar: String): String { - return $MULTILINE_STRING_QUOTE + @Nested + inner class `Given non-ktlint_official code style` { + private val nonKtlintOfficialCodeStyle = CodeStyleValue.android_studio + + @Test + fun `As body expression on same line as equals and preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As body expression on same line as equals but not preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) =$MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As block body`() { + val code = + """ + private fun foo( bar: String): String { + return $MULTILINE_STRING_QUOTE + bar $MULTILINE_STRING_QUOTE.trimIndent() - } - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As body expression of function wrapped in class`() { + val code = + """ + class Bar { + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } } - @Test - fun `As body expression of function wrapped in class`() { - val code = - """ - class Bar { + @Nested + inner class `Given ktlint_official code style` { + @Test + fun `As body expression on same line as equals and preceded by space`() { + val code = + """ private fun foo( bar: String, ) = $MULTILINE_STRING_QUOTE bar $MULTILINE_STRING_QUOTE.trimIndent() - } - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As body expression on same line as equals but not preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) =$MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As block body`() { + val code = + """ + private fun foo( bar: String): String { + return $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As body expression of function wrapped in class`() { + val code = + """ + class Bar { + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } } } + @Test + fun `Issue 1993 - An or operator at start of line followed by a dot qualified expressions should not throw an exception`() { + val code = + """ + val foo = + if (false + || foobar.bar() + ) { + // Do something + } + """.trimIndent() + val formattedCode = + """ + val foo = + if (false || + foobar.bar() + ) { + // Do something + } + """.trimIndent() + indentationRuleAssertThat(code) + .addAdditionalRuleProvider { ChainWrappingRule() } + .hasLintViolationForAdditionalRule(3, 9, "Line must not begin with \"||\"") + .isFormattedAs(formattedCode) + } + private companion object { val INDENT_STYLE_TAB = INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt index db28d360e5..f93ee0b63a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt @@ -212,4 +212,13 @@ class NoConsecutiveBlankLinesRuleTest { """.trimIndent(), ) } + + @Test + fun `Issue 1987 - Class without body but followed by multiple blank lines until end of file should not throw exception`() { + val code = "class Foo\n\n\n" + val formattedCode = "class Foo\n" + noConsecutiveBlankLinesRuleAssertThat(code) + .hasLintViolations(LintViolation(3, 1, "Needless blank line(s)")) + .isFormattedAs(formattedCode) + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt new file mode 100644 index 0000000000..e2caad3f74 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -0,0 +1,136 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Test + +class NoEmptyFileRuleTest { + private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } + + @Test + fun `Given non-empty kotlin file then ignore the rule for this file`() { + val code = + """ + package foo + fun main() { + println("foo") + } + """.trimIndent() + noEmptyFileRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given an empty kotlin file then do a return lint error`() { + val code = EMPTY_FILE + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given an empty kotlin script file then do a return lint error`() { + val code = EMPTY_FILE + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kts") + .asKotlinScript() + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kts' should not be empty") + } + + @Test + fun `Given only package statement in kotlin file then do a return lint error`() { + val code = + """ + package foo + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given only import statement in kotlin file then do a return lint error`() { + val code = + """ + import foo.Bar + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given only package and import statements in kotlin file then do a return lint error`() { + val code = + """ + package foo + import foo.Bar + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given only package, import statements and comments in kotlin file then do a return lint error`() { + val code = + """ + package foo + import foo.Bar + + // some comment + + /* + * some comment + */ + + /** + * some comment + */ + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given non-empty kotlin file then ignore this file`() { + val code = + """ + package foo + fun main() { + println("foo") + } + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasNoLintViolations() + } + + @Test + fun `x x`() { + val code = + """ + plugins { + id("ktlint-publication-library") + } + + dependencies { + implementation(projects.ktlintLogger) + implementation(projects.ktlintRuleEngine) + implementation(projects.ktlintCliRulesetCore) + api(libs.assertj) + api(libs.junit5) + api(libs.janino) + api(libs.jimfs) + } + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kts") + .asKotlinScript() + .hasNoLintViolations() + } + + private companion object { + private const val EMPTY_FILE = "" + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt new file mode 100644 index 0000000000..68eefbd98b --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt @@ -0,0 +1,179 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class NoSingleLineBlockCommentRuleTest { + private val noSingleLineBlockCommentRuleAssertThat = + assertThatRule( + provider = { NoSingleLineBlockCommentRule() }, + additionalRuleProviders = setOf(RuleProvider { CommentWrappingRule() }), + editorConfigProperties = setOf(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official), + ) + + @Test + fun `Given a single line block comment then replace it with an EOL comment`() { + val code = + """ + fun bar() { + /* Some comment */ + } + """.trimIndent() + val formattedCode = + """ + fun bar() { + // Some comment + } + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasLintViolation(2, 5, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a multi line block comment that start starts and end on a separate line then do not reformat`() { + val code = + """ + /* + * Some comment + */ + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a single line block comment that is to be wrapped before replacing it with an EOL comment`() { + val code = + """ + /* Some comment */ val foo = "foo" + """.trimIndent() + val formattedCode = + """ + // Some comment + val foo = "foo" + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolationForAdditionalRule(1, 20, "A block comment may not be followed by any other element on that same line") + // Can not check for the lint violation below as it will only be thrown while formatting with comment wrapping + // A single line block comment must be replaced with an EOL comment + .isFormattedAs(formattedCode) + } + + @Nested + inner class `Given some code code followed by a block comment on the same line` { + @Test + fun `Given a comment followed by a property and separated with space`() { + val code = + """ + val foo = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 17, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a property but not separated with space`() { + val code = + """ + val foo = "foo"/* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 16, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a function and separated with space`() { + val code = + """ + fun foo() = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + fun foo() = "foo" // Some comment + """.trimIndent() + @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 19, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a function but not separated with space`() { + val code = + """ + fun foo() = "foo"/* Some comment */ + """.trimIndent() + val formattedCode = + """ + fun foo() = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 18, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + } + + @Test + fun `Given a block comment that does not contain a newline and which is after some code om the same line is changed to an EOL comment`() { + val code = + """ + val foo = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 17, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a single line block comment in between code elements on the same line does not raise a lint error`() { + val code = + """ + val foo /* some comment */ = "foo" + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code).hasNoLintViolationsExceptInAdditionalRules() + } + + @Test + fun `Given a single line block containing a block comment then do not reformat`() { + val code = + """ + val foo = { /* no-op */ } + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `Given single line block comments to disable or enable ktlint then do not reformat`() { + val code = + """ + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasNoLintViolations() + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt index 5cc3aba446..d96a3ee426 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt @@ -179,7 +179,7 @@ class ParameterListWrappingRuleTest { } @Nested - inner class `Given a function literal having a multiline parameter list` { + inner class `Given a function literal having a multiline parameter list and the first parameter starts on same line as LBRACE` { private val code = """ val fieldExample = @@ -204,10 +204,9 @@ class ParameterListWrappingRuleTest { """.trimIndent() parameterListWrappingRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasLintViolationsForAdditionalRule( - LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"), - LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"), - ).isFormattedAs(formattedCode) + // Indent violations will not be reported until after the wrapping of the first parameter is completed and as of that will + // not be found during linting + .isFormattedAs(formattedCode) } @ParameterizedTest(name = "Code style = {0}") @@ -216,17 +215,35 @@ class ParameterListWrappingRuleTest { mode = EnumSource.Mode.EXCLUDE, names = ["ktlint_official"], ) - fun `Given another than ktlint_official code style then do not reformat`(codeStyleValue: CodeStyleValue) { + fun `Given another code style than ktlint_official then do not reformat`(codeStyleValue: CodeStyleValue) { parameterListWrappingRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyleValue) .hasNoLintViolations() -// .hasLintViolationsForAdditionalRule( -// LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"), -// LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"), -// ) } } + @ParameterizedTest(name = "Code style = {0}") + @EnumSource(value = CodeStyleValue::class) + fun `Given a multiline reference expression with trailing lambda having a multiline parameter list and the first parameter starts on same line as LBRACE`( + codeStyleValue: CodeStyleValue, + ) { + val code = + """ + val foo = + bar( + Any(), + Any() + ) { a, + b + -> + foobar() + } + """.trimIndent() + parameterListWrappingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyleValue) + .hasNoLintViolations() + } + @Test fun `Given a function with annotated parameters then start each parameter on a separate line but preserve spacing between annotation and parameter name`() { val code = diff --git a/ktlint-test/build.gradle.kts b/ktlint-test/build.gradle.kts index 8e492dcdb1..6262c050d4 100644 --- a/ktlint-test/build.gradle.kts +++ b/ktlint-test/build.gradle.kts @@ -8,7 +8,6 @@ dependencies { implementation(projects.ktlintCliRulesetCore) api(libs.assertj) api(libs.junit5) - api(libs.logback) api(libs.janino) api(libs.jimfs) } diff --git a/run-mkdocs-server.sh b/run-mkdocs-server.sh deleted file mode 100755 index a4be213a57..0000000000 --- a/run-mkdocs-server.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Run mkdocs server. Terminate with CTRL-C" -echo -docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material diff --git a/settings.gradle.kts b/settings.gradle.kts index 336cf04026..802a76d912 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,6 @@ include( // ktlint-core module is no longer used internally in the ktlint project except for backwards compatibility ":ktlint-core", ":ktlint-api-consumer", - ":ktlint-baseline", ":ktlint-bom", ":ktlint-cli", ":ktlint-cli-reporter-baseline",