diff options
author | Aurimas Liutikas <aurimas@google.com> | 2018-05-25 16:15:10 -0700 |
---|---|---|
committer | Aurimas Liutikas <aurimas@google.com> | 2018-05-25 16:15:10 -0700 |
commit | 3432676ef4ed599933a9a05a9f4e712e7e657f1b (patch) | |
tree | 8aa62a48ff99be9923c5f57521ba855b763eb8aa | |
parent | 946fa4217826656ac2e82c101f63c5c471ee5df0 (diff) | |
parent | 326cdc53aa67dc3e8d68eb24fcc08b3e4a7b9bde (diff) | |
download | ktlint-master.tar.gz |
Update to ktlint 0.23.1
https://github.com/shyiko/ktlint/tree/0.23.1
Test: None
112 files changed, 4343 insertions, 824 deletions
diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..a848655a --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,19 @@ +version: '{branch}.{build}' + +skip_tags: true + +environment: + matrix: + - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 + +install: + - cmd: echo %JAVA_HOME% + - cmd: echo %M2_HOME% + +test_script: + - mvn clean verify + +cache: + - C:\Users\appveyor\.m2 + +build: off @@ -2,3 +2,5 @@ target .gradle build !ktlint/src/main/resources/config/.idea +/.idea +*.iml diff --git a/.travis.yml b/.travis.yml index 108bf07b..606bfc18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,12 @@ language: java jdk: - - oraclejdk8 -script: ./mvnw clean verify +- oraclejdk8 +script: if [[ $TRAVIS_REPO_SLUG == "shyiko/ktlint" && $TRAVIS_BRANCH == "master" && + $TRAVIS_PULL_REQUEST == "false" ]]; then ./mvnw -Ddeploy=maven-central -DskipStaging=true -Dgpg.skip=true && + ./mvnw -Ddeploy=github -Dgpg.skip=true -Dgithub.draft=true -Dgithub.description="https://github.com/shyiko/ktlint#access-to-the-latest-master-snapshot"; else ./mvnw clean verify; + fi +env: + global: + - secure: ApIfhi3JFZSZXoy5jwSev6AAx85PM5ABl2+RvUokon/xv5ztxX+hTKYMQ068BpYW7glegC+qvk1RQSHZ2zzD95lFngu7saZUWk7g/fqg92lqz5KIicIbvJ8h1An/8vuCq+mg9Of9Io1oBFF0e4RuzoCG7DIks4oaEypOujnadSx+cks9sZpNwXNHjccc3XyTKieFr3WIa9rcrNmkTqojJToRUxKAt5+XU2GLDSgqj8a8EtE/n/1fM3voYOdj2aYFl+P1zPZ4kKliP68rkdpe6kCGCMcJFt3hXpqGs8X40T+8nClARK9kSi9KgQdz/Qbs3Gb5jh7JWc31G8gWFx2g2RQX0edKD0udygmOAAFNBPJIKrmHnWSe/oyPxBHnW2eYb7w1LvCVC4LtlErXrKcXiz0PXjMuBuK4N82RHIHzVeANiewRoiMvxFo8sfmTtCiu+Nx7+CiIMoxP5P3VS5GN++nctFIHvqIqvSQElFdk0rYrrFSEk9ct+NVvhw3UHwAHt1gbjq/dUayLrljGBPIAsHy1GOe0fX56dX8xHLJO2VM+eWAbNYiFyQxhFCI1LOv+PKiW8diQ+txP3EoXclBZbYDPQqZ/RlCKCcGwJNQqhMSPciYzjZrV7vLk4lzpuCM26IEBMr5Oyeu0zlMWQI7sI6V/BUCejPK2IkrFweEYHws= + - secure: ztSej57b5xzWgCVOBBFrw8dmjwIVkBPGZwwpIdB/OXcN4xLM3dxWQaq1ADxzOqNdaLb3GOTJ0VJpdAqcjlQjTaq8PZyLULaB44cxiCdi9huNpHwfdUBWsJqD4mg3CQzf7jmAuUtf/aaP32WC0r5kH/Crn1awwatdGwuOZyPfpK6H9dP4KPDytClPnR/GO0WRyxk1t0OeRM01NCY6E69xiKEqC1q+Y0bu5GeVs8uQiSJchf5O8B/jBP74Gwi8cV5hPvfK9onbqCpI/ecen5dR+Z3hGl71eO20yU0iH5jC2sksKaPsvYW6zLJb0ederzfR92h2mFA2HlPtYjKibmvgzTBaOXqUCnUCPpnszgCtJ2W4gJPzRaNlba0GqqUakdrwmhdojbvEMcoF0cRUlvLI5nO1WE1V3OIno5R4q5tY1DydO7nqF2RJ/yDVjKI4S7kyPnZdSSY9FgRM5xnWPsuVS59oBmVfBaRoa+qv9lEWMSTL0NBAYtqrmjKQWGBuSqHBBdEuiGIWsrC+Z2zVEmUe+4U3GNSfFnHgGr5YoJuCvIxS6qU2KUcLHimVMvprXNHUmCgr2J+PFaoVwHH1yLSyIiNy7fRiO0nsGssCApqW6Yy3fFLUbjOmzBrpog94lKs4b+5V/MV6nQsRcqz53poHOVbyZF2mkyxOJQyUbpUOAFA= + - secure: 0QUfiKxjMTE4FaCmbYT5Jm8Fuez5Pw/BjvNdPSHjpEp0Cz+/7KQeSYB30lG3LxhW48gAoXrJebAHZZvCKMDZBaFicqueeYHx+CO8XqNSUdXqvARHzL/QtIeP3+uLKAuNq/sfUqu+uPCcPl193aIQ21inW0+1X9VL2map7JRRm/7tyc8zEjWOyCKEwm5yTj7sdpY1Si03RPqVvGwxb+i7z4qbEFJeY3bfAtRgFXnwn5Z6OZ0T9NzdLFFYooKO0wfFbsJz4nBGZakVGLJa4qjVYf8F/PTqDDdFGufCblaaZnn5rFHg1ENGHlZJPKLd15doyeEsY3boCd2Fh25m7U97HT5Nw/1wpI6cdC718YVnCntsCB1YgtCYHLDMGogLKYFsmkessjg6h7dswaj4Q9YtlxZcjzfVTeHzEvCMQUDt83ugeU6SjetBZKHm0JmZ4Y7JIZdNbYz4Umhb7mn2qUHaAiGROTF6iOmEfS1V1Ha0utue+lxcm0GSr4gk1wnbeE6CinlRrqW9Xq17y0umWucCRQdOCem1fX13AORzzzOMGdUHvWzZw6UmJN6C+uqFcvMK/ze7IIlOiOcIkyfApMViv/oziYlarEH3DuL34hHrg2StUdMJwbb4cGIxlnoRxAHEASWkg0Ak69HMWgtVCZgw/HVozrxlb9ZpWrgAgCIQG/4= diff --git a/CHANGELOG.md b/CHANGELOG.md index 41af5437..6da54b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,157 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.23.0] - 2018-05-02 + +### Added +- `comment-spacing` ([#198](https://github.com/shyiko/ktlint/pull/198)), + `filename` ([#194](https://github.com/shyiko/ktlint/pull/194)) rules. +- `parameter-list-wrapping` left parenthesis placement check ([#201](https://github.com/shyiko/ktlint/pull/201)). +- `parameter-list-wrapping` auto-correction when `max_line_length` is exceeded ([#200](https://github.com/shyiko/ktlint/pull/200)). + +### Fixed +- "Unused import" false positive (x.y.zNNN import inside x.y.z package) ([#204](https://github.com/shyiko/ktlint/issues/204)). + +### Changed +- `kotlin-compiler` version to 1.2.41 (from 1.2.40). + +## [0.22.0] - 2018-04-22 + +### Added +- `--apply-to-idea-project` (as an alternative to (global) `--apply-to-idea`) ([#178](https://github.com/shyiko/ktlint/issues/178)). +- Check to verify that annotations are placed before the modifiers ([#183](https://github.com/shyiko/ktlint/pull/183)). +- Access to PsiFile location information ([#194](https://github.com/shyiko/ktlint/pull/194)). + +### Fixed +- `--format` commenting out operators (`chain-wrapping` rule) ([#193](https://github.com/shyiko/ktlint/pull/193)). + +### Changed +- `indent` rule (`continuation_indent_size` is now ignored) ([#171](https://github.com/shyiko/ktlint/issues/171)). +NOTE: if you have a custom `continuation_indent_size` (and `gcd(indent_size, continuation_indent_size) == 1`) ktlint +won't check the indentation. +- `--apply-to-idea` to inherit "Predefined style / Kotlin style guide" (Kotlin plugin 1.2.20+). +- `kotlin-compiler` version to 1.2.40 (from 1.2.30). + +## [0.21.0] - 2018-03-29 + +### Changed +- `indent` rule to ignore `where <type constraint list>` clause ([#180](https://github.com/shyiko/ktlint/issues/180)). + +## [0.20.0] - 2018-03-20 + +### Added +- Ability to load 3rd party reporters from the command-line (e.g. `--reporter=<name>,artifact=<group_id>:<artifact_id>:<version>`) ([#176](https://github.com/shyiko/ktlint/issues/176)). +- `--ruleset`/`--reporter` dependency tree validation. + +### Fixed +- Handling of spaces in `--reporter=...,output=<path_to_a_file>` ([#177](https://github.com/shyiko/ktlint/issues/177)). +- `+`, `-`, `*`, `/`, `%`, `&&`, `||` wrapping ([#168](https://github.com/shyiko/ktlint/issues/168)). + +### Changed +- `comma-spacing` rule to be more strict ([#173](https://github.com/shyiko/ktlint/issues/173)). +- `no-line-break-after-else` rule to allow multi-line `if/else` without curly braces. + +## [0.19.0] - 2018-03-04 + +### Changed +- Lambda formatting: if lambda is assigned a label, there should be no space between the label and the opening curly brace ([#167](https://github.com/shyiko/ktlint/issues/167)). + +## [0.18.0] - 2018-03-01 + +### Added +- Java 9 support ([#152](https://github.com/shyiko/ktlint/issues/152)). + +### Changed +- `kotlin-compiler` version to 1.2.30 (from 1.2.20). + +## [0.17.1] - 2018-02-28 + +### Fixed +- `Internal Error (parameter-list-wrapping)` when `indent_size=unset` ([#165](https://github.com/shyiko/ktlint/issues/165)). + +## [0.17.0] - 2018-02-28 + +### Fixed +- `+`/`-` wrapping inside `catch` block, after `else` and `if (..)` ([#160](https://github.com/shyiko/ktlint/issues/160)). +- Multi-line parameter declaration indentation ([#161](https://github.com/shyiko/ktlint/issues/161)). +- Expected indentation reported by `indent` rule. + +### Changed +- Error code returned by `ktlint --format/-F` when some of the errors cannot be auto-corrected (previously it was 0 instead of expected 1) ([#162](https://github.com/shyiko/ktlint/issues/162)). + +## [0.16.1] - 2018-02-27 + +### Fixed +- Handling of negative number condition in `when` block ([#160](https://github.com/shyiko/ktlint/issues/160)). + +## [0.16.0] - 2018-02-27 + +### Added +- `parameter-list-wrapping` rule ([#130](https://github.com/shyiko/ktlint/issues/130)). +- `+`, `-`, `*`, `/`, `%`, `&&`, `||` wrapping check (now part of `chain-wrapping` rule). + +### Fixed +- Unused `componentN` import (where N > 5) false positive ([#142](https://github.com/shyiko/ktlint/issues/142)). +- max-line-length error suppression ([#158](https://github.com/shyiko/ktlint/issues/158)). + +### Changed +- `modifier-order` rule to match official [Kotlin Coding Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#modifiers) ([#146](https://github.com/shyiko/ktlint/issues/146)) +(`override` modifier should be placed before `suspend`/`tailrec`, not after) + +## [0.15.1] - 2018-02-14 + +### Fixed +- Race condition when multiple rules try to modify AST node that gets detached as a result of mutation ([#154](https://github.com/shyiko/ktlint/issues/154)). + +## [0.15.0] - 2018-01-18 + +### Added +- `no-line-break-after-else` rule ([#125](https://github.com/shyiko/ktlint/issues/125)). + +### Changed +- `kotlin-compiler` version to 1.2.20 (from 1.2.0). + +## [0.14.0] - 2017-11-30 + +### Changed +- `continuation_indent_size` to 4 when `--android` profile is used ([android/kotlin-guides#37](https://github.com/android/kotlin-guides/issues/37)). + +### Fixed +- Maven integration ([#117](https://github.com/shyiko/ktlint/issues/117)). + +## [0.13.0] - 2017-11-28 + +### Added +- `no-line-break-before-assignment` ([#105](https://github.com/shyiko/ktlint/issues/105)), + `chain-wrapping` ([#23](https://github.com/shyiko/ktlint/issues/23)) +(when wrapping chained calls `.`, `?.` and `?:` should be placed on the next line), + `range-spacing` (no spaces around range (`..`) operator) rules. +- `--print-ast` CLI option which can be used to dump AST of the file +(see [README / Creating a ruleset / AST](https://github.com/shyiko/ktlint#ast) for more details) +- `--color` CLI option for colored output (where supported, e.g. --print-ast, default (plain) reporter, etc) + +### Changed +- `.editorconfig` property resolution. +An explicit `[*.{kt,kts}]` is not required anymore (ktlint looks for sections +containing `*.kt` (or `*.kts`) and will fallback to `[*]` whenever property cannot be found elsewhere). +Also, a search for .editorconfig will no longer stop on first (closest) `.editorconfig` (unless it contains `root=true`). +- `max-line-length` rule to assume `max_line_length=100` when `ktlint --android ...` is used +(per [Android Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html)). +- `kotlin-compiler` version to 1.2.0 (from 1.1.51). + +### Fixed +- `no-empty-class-body` auto-correction at the end of file ([#109](https://github.com/shyiko/ktlint/issues/109)). +- `max-line-length` rule when applied to KDoc ([#112](https://github.com/shyiko/ktlint/issues/112)) +(previously KDoc was subject to `max-line-length` even though regular comments were not). +- Spacing around `=` in @annotation|s (`op-spacing`). +- Spacing around generic type parameters of functions (e.g. `fun <T>f(): T {}` -> `fun <T> f(): T {}`). +- `no-consecutive-blank-lines` not triggering at the end of file (when exactly 2 blank lines are present) ([#108](https://github.com/shyiko/ktlint/issues/108)) +- `indent` `continuation_indent_size % indent_size != 0` case ([#76](https://github.com/shyiko/ktlint/issues/76)) +- `indent` rule skipping first parameter indentation check. +- `final-newline` rule in the context of kotlin script. +- Git hook (previously files containing space character (among others) in their names were ignored) +- Exit code when file cannot be linted due to the invalid syntax or internal error. + ## [0.12.1] - 2017-11-13 ### Fixed @@ -243,6 +394,20 @@ set in `[*{kt,kts}]` section). ## 0.1.0 - 2016-07-27 +[0.23.0]: https://github.com/shyiko/ktlint/compare/0.22.0...0.23.0 +[0.22.0]: https://github.com/shyiko/ktlint/compare/0.21.0...0.22.0 +[0.21.0]: https://github.com/shyiko/ktlint/compare/0.20.0...0.21.0 +[0.20.0]: https://github.com/shyiko/ktlint/compare/0.19.0...0.20.0 +[0.19.0]: https://github.com/shyiko/ktlint/compare/0.18.0...0.19.0 +[0.18.0]: https://github.com/shyiko/ktlint/compare/0.17.1...0.18.0 +[0.17.1]: https://github.com/shyiko/ktlint/compare/0.17.0...0.17.1 +[0.17.0]: https://github.com/shyiko/ktlint/compare/0.16.1...0.17.0 +[0.16.1]: https://github.com/shyiko/ktlint/compare/0.16.0...0.16.1 +[0.16.0]: https://github.com/shyiko/ktlint/compare/0.15.1...0.16.0 +[0.15.1]: https://github.com/shyiko/ktlint/compare/0.15.0...0.15.1 +[0.15.0]: https://github.com/shyiko/ktlint/compare/0.14.0...0.15.0 +[0.14.0]: https://github.com/shyiko/ktlint/compare/0.13.0...0.14.0 +[0.13.0]: https://github.com/shyiko/ktlint/compare/0.12.1...0.13.0 [0.12.1]: https://github.com/shyiko/ktlint/compare/0.12.0...0.12.1 [0.12.0]: https://github.com/shyiko/ktlint/compare/0.11.1...0.12.0 [0.11.1]: https://github.com/shyiko/ktlint/compare/0.11.0...0.11.1 @@ -6,7 +6,8 @@ <p align="center"> <a href="https://travis-ci.org/shyiko/ktlint"><img src="https://travis-ci.org/shyiko/ktlint.svg?branch=master" alt="Build Status"></a> -<a href="http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.shyiko%22%20AND%20a%3A%22ktlint%22"><img src="https://img.shields.io/maven-central/v/com.github.shyiko/ktlint.svg" alt="Maven Central"></a> +<a href="https://ci.appveyor.com/project/shyiko/ktlint"><img src="https://ci.appveyor.com/api/projects/status/9dtlak3cj5rum48g?svg=true&passingText=passing" alt="Build Status"></a> +<a href="https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.shyiko%22%20AND%20a%3A%22ktlint%22"><img src="https://img.shields.io/maven-central/v/com.github.shyiko/ktlint.svg" alt="Maven Central"></a> <a href="https://ktlint.github.io/"><img src="https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg" alt="ktlint"></a> </p> @@ -38,11 +39,16 @@ It's also [easy to create your own](#creating-a-reporter). - No trailing whitespaces; - No `Unit` returns (`fun fn {}` instead of `fun fn: Unit {}`); - No empty (`{}`) class bodies; +- No spaces around range (`..`) operator; +- No newline before (binary) `+` & `-`, `*`, `/`, `%`, `&&`, `||`; +- When wrapping chained calls `.`, `?.` and `?:` should be placed on the next line; +- When a line is broken at an assignment (`=`) operator the break comes after the symbol; +- When class/function signature doesn't fit on a single line, each parameter must be on a separate line; - Consistent string templates (`$v` instead of `${v}`, `${p.v}` instead of `${p.v.toString()}`); - Consistent order of modifiers; -- Consistent spacing after keywords, commas; around colons, curly braces, infix operators, etc; -- Newline at the end of each file -(unless `insert_final_newline` is set to false in .editorconfig (see [EditorConfig](#editorconfig) section for more)). +- Consistent spacing after keywords, commas; around colons, curly braces, infix operators, comments, etc; +- Newline at the end of each file (not enabled by default, but recommended) +(set `insert_final_newline=true` in .editorconfig to enable (see [EditorConfig](#editorconfig) section for more)). ## EditorConfig @@ -52,9 +58,12 @@ ktlint recognizes the following [.editorconfig](http://editorconfig.org/) proper [*.{kt,kts}] # possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely) indent_size=4 -continuation_indent_size=8 -insert_final_newline=true -# possible values: number (e.g. 120) (package name, imports & comments are ignored), "off" +# possible values: number (e.g. 2), "unset" +continuation_indent_size=4 +# true (recommended) / false +insert_final_newline=unset +# possible values: number (e.g. 120) (package name, imports & comments are ignored), "off" +# it's automatically set to 100 on `ktlint --android ...` (per Android Kotlin Style Guide) max_line_length=off ``` @@ -63,14 +72,14 @@ max_line_length=off > Skip all the way to the "Integration" section if you don't plan to use `ktlint`'s command line interface. ```sh -curl -sSLO https://github.com/shyiko/ktlint/releases/download/0.12.1/ktlint && +curl -sSLO https://github.com/shyiko/ktlint/releases/download/0.23.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ ``` ... or just download `ktlint` from the [releases](https://github.com/shyiko/ktlint/releases) page (`ktlint.asc` contains PGP signature which you can verify with `curl -sS https://keybase.io/shyiko/pgp_keys.asc | gpg --import && gpg --verify ktlint.asc`). -On macOS ([or Linux](http://linuxbrew.sh/)) you can also use [brew](http://brew.sh/) - `brew install shyiko/ktlint/ktlint`. +On macOS ([or Linux](http://linuxbrew.sh/)) you can also use [brew](https://brew.sh/) - `brew install shyiko/ktlint/ktlint`. > If you don't have curl installed - replace `curl -sL` with `wget -qO-`. @@ -84,7 +93,7 @@ Usually simple `http_proxy=http://proxy-server:port https_proxy=http://proxy-ser ```bash # check the style of all Kotlin files inside the current dir (recursively) # (hidden folders will be skipped) -$ ktlint +$ ktlint --color src/main/kotlin/Main.kt:10:10: Unused import # check only certain locations (prepend ! to negate the pattern) @@ -118,7 +127,7 @@ $ ktlint --install-git-pre-commit-hook <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> + <version>1.8</version> <executions> <execution> <id>ktlint</id> @@ -126,7 +135,7 @@ $ ktlint --install-git-pre-commit-hook <configuration> <target name="ktlint"> <java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true" - classname="com.github.shyiko.ktlint.Main" classpathref="maven.plugin.classpath"> + classpathref="maven.plugin.classpath" classname="com.github.shyiko.ktlint.Main"> <arg value="src/**/*.kt"/> <!-- to generate report in checkstyle format prepend following args: --> <!-- @@ -144,7 +153,7 @@ $ ktlint --install-git-pre-commit-hook <configuration> <target name="ktlint"> <java taskname="ktlint" dir="${basedir}" fork="true" failonerror="true" - classname="com.github.shyiko.ktlint.Main" classpathref="maven.plugin.classpath"> + classpathref="maven.plugin.classpath" classname="com.github.shyiko.ktlint.Main"> <arg value="-F"/> <arg value="src/**/*.kt"/> </java> @@ -157,7 +166,7 @@ $ ktlint --install-git-pre-commit-hook <dependency> <groupId>com.github.shyiko</groupId> <artifactId>ktlint</artifactId> - <version>0.12.1</version> + <version>0.23.0</version> </dependency> <!-- additional 3rd party ruleset(s) can be specified here --> </dependencies> @@ -170,13 +179,16 @@ To run formatter - `mvn antrun:run@ktlint-format`. #### ... with [Gradle](https://gradle.org/) +#### (without a plugin) + > build.gradle ```groovy -apply plugin: "java" +// kotlin-gradle-plugin must be applied for configuration below to work +// (see https://kotlinlang.org/docs/reference/using-gradle.html) repositories { - mavenCentral() + jcenter() } configurations { @@ -184,16 +196,16 @@ configurations { } dependencies { - ktlint "com.github.shyiko:ktlint:0.12.1" + ktlint "com.github.shyiko:ktlint:0.23.0" // additional 3rd party ruleset(s) can be specified here - // just add them to the classpath (ktlint 'groupId:artifactId:version') and + // just add them to the classpath (e.g. ktlint 'groupId:artifactId:version') and // ktlint will pick them up } task ktlint(type: JavaExec, group: "verification") { description = "Check Kotlin code style." - main = "com.github.shyiko.ktlint.Main" classpath = configurations.ktlint + main = "com.github.shyiko.ktlint.Main" args "src/**/*.kt" // to generate report in checkstyle format prepend following args: // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" @@ -203,22 +215,25 @@ check.dependsOn ktlint task ktlintFormat(type: JavaExec, group: "formatting") { description = "Fix Kotlin code style deviations." - main = "com.github.shyiko.ktlint.Main" classpath = configurations.ktlint + main = "com.github.shyiko.ktlint.Main" args "-F", "src/**/*.kt" } ``` -> Note: For an Android project this config would typically go into your app/build.gradle. - To check code style - `gradle ktlint` (it's also bound to `gradle check`). To run formatter - `gradle ktlintFormat`. -**Another option** is to use Gradle plugin (in order of appearance): -- [jlleitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) -- [jeremymailen/kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) +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. + +#### (with a plugin) -Each plugin has some unique features (like incremental build support in case of [jeremymailen/kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle)) so check them out. +Gradle plugins (in order of appearance): +- [jlleitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) +The very first ktlint gradle plugin. + +- [jeremymailen/kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) +Gradle plugin featuring incremental build, `*.kts` support. You might also want to take a look at [diffplug/spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle#applying-ktlint-to-kotlin-files) which has a built-in support for ktlint. In addition to linting/formatting kotlin code it allows you to keep license headers, markdown documentation, etc. in check. @@ -232,24 +247,29 @@ You might also want to take a look at [diffplug/spotless](https://github.com/dif > (inside project's root directory) ```sh -ktlint --apply-to-idea +ktlint --apply-to-idea-project # or if you want to be compliant with Android Kotlin Style Guide -ktlint --apply-to-idea --android +ktlint --apply-to-idea-project --android ``` ##### Option #2 -Go to `File -> Settings... -> Editor` -- `General -> Auto Import` +Go to <kbd>File</kbd> -> <kbd>Settings...</kbd> -> <kbd>Editor</kbd> +- <kbd>General</kbd> -> <kbd>Auto Import</kbd> - check `Optimize imports on the fly (for current project)`. -- `Code Style -> Kotlin` - - open `Imports` tab, select all `Use single name import` options and remove `import java.util.*` from `Packages to Use Import with '*'`. - - open `Blank Lines` tab, change `Keep Maximum Blank Lines` -> `In declarations` & `In code` to 1 and `Before '}'` to 0. - - (optional but recommended) open `Wrapping and Braces` tab, uncheck `Method declaration parameters -> Align when multiline`. - - (optional but recommended) open `Tabs and Indents` tab, change `Continuation indent` to 4 (to be compliant with - [Android Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html) value should stay equal 8). -- `Inspections` - - change `Severity` level of `Unused import directive`, `Redundant semicolon` and (optional but recommended) `Unused symbol` to `ERROR`. +- <kbd>Code Style</kbd> -> <kbd>Kotlin</kbd> + - <kbd>Set from...</kbd> -> <kbd>Predefined style</kbd> -> <kbd>Kotlin style guide</kbd> (Kotlin plugin 1.2.20+). + - open <kbd>Imports</kbd> tab + - select `Use single name import` (all of them); + - remove `import java.util.*` from `Packages to Use Import with '*'`. + - open <kbd>Blank Lines</kbd> tab + - change `Keep Maximum Blank Lines` / `In declarations` & `In code` to 1 and `Before '}'` to 0. + - (optional but recommended) open <kbd>Wrapping and Braces</kbd> tab + - uncheck `Method declaration parameters` / `Align when multiline`. + - (optional but recommended) open <kbd>Tabs and Indents</kbd> tab + - change `Continuation indent` to the same value as `Indent` (4 by default). +- <kbd>Inspections</kbd> + - change `Severity` level of `Unused import directive` and `Redundant semicolon` to `ERROR`. #### ... with [GNU Emacs](https://www.gnu.org/software/emacs/) @@ -279,6 +299,39 @@ $ ktlint -R com.github.username:rulseset:master-SNAPSHOT A complete sample project (with tests and build files) is included in this repo under the [ktlint-ruleset-template](ktlint-ruleset-template) directory (make sure to check [NoVarRuleTest](ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt) as it contains some useful information). +#### AST + +While writing/debugging [Rule](ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/Rule.kt)s it's often helpful to have an AST +printed out to see the structure rules have to work with. ktlint >= 0.15.0 has `--print-ast` flag specifically for this purpose +(usage: `ktlint --color --print-ast <file>`). +An example of the output is shown below. + +```sh +$ printf "fun main() {}" | ktlint --color --print-ast --stdin + +1: ~.psi.KtFile (~.psi.stubs.elements.KtFileElementType.kotlin.FILE) +1: ~.psi.KtPackageDirective (~.psi.stubs.elements.KtPlaceHolderStubElementType.PACKAGE_DIRECTIVE) "" +1: ~.psi.KtImportList (~.psi.stubs.elements.KtPlaceHolderStubElementType.IMPORT_LIST) "" +1: ~.psi.KtScript (~.psi.stubs.elements.KtScriptElementType.SCRIPT) +1: ~.psi.KtBlockExpression (~.KtNodeType.BLOCK) +1: ~.psi.KtNamedFunction (~.psi.stubs.elements.KtFunctionElementType.FUN) +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtKeywordToken.fun) "fun" +1: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " " +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtToken.IDENTIFIER) "main" +1: ~.psi.KtParameterList + (~.psi.stubs.elements.KtPlaceHolderStubElementType.VALUE_PARAMETER_LIST) +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.LPAR) "(" +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.RPAR) ")" +1: ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (~.c.i.p.tree.IElementType.WHITE_SPACE) " " +1: ~.psi.KtBlockExpression (~.KtNodeType.BLOCK) +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.LBRACE) "{" +1: ~.c.i.p.impl.source.tree.LeafPsiElement (~.lexer.KtSingleValueToken.RBRACE) "}" + + format: <line_number:> <node.psi::class> (<node.elementType>) "<node.text>" + legend: ~ = org.jetbrains.kotlin, c.i.p = com.intellij.psi + +``` + ## Creating a reporter Take a look at [ktlint-reporter-plain](ktlint-reporter-plain). @@ -288,7 +341,7 @@ In short, all you need to do is to implement a a custom [ReporterProvider](ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/ReporterProvider.kt) using `META-INF/services/com.github.shyiko.ktlint.core.ReporterProvider`. Pack all of that into a JAR and you're done. -To load a custom (3rd party) reporter use `ktlint --reporter=groupId:artifactId:version` / `ktlint --reporter=/path/to/custom-ktlint-reporter.jar` +To load a custom (3rd party) reporter use `ktlint --reporter=name,artifact=groupId:artifactId:version` / `ktlint --reporter=name,artifact=/path/to/custom-ktlint-reporter.jar` (see `ktlint --help` for more). ## Badge @@ -349,6 +402,39 @@ git clone https://github.com/shyiko/ktlint && cd ktlint ./mvnw # shows how to build, test, etc. project ``` +#### Access to the latest `master` snapshot + +Whenever a commit is added to the `master` branch `0.0.0-SNAPSHOT` is automatically uploaded to [Sonatype's snapshots repository](https://oss.sonatype.org/content/repositories/snapshots/com/github/shyiko/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 `0.0.0-SNAPSHOT` + adding a repo: + +##### Maven + +```xml +... +<repository> + <id>sonatype-snapshots</id> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + <releases> + <enabled>false</enabled> + </releases> +</repository> +... +``` + +##### Gradle + +```groovy +repositories { + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } +} +``` + ## Legal This project is not affiliated with or endorsed by the Jetbrains. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..c1c9c6b8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.2.40" apply false +} + +ext.libraries = [ + "kotlin_stdlib": "org.jetbrains.kotlin:kotlin-stdlib:1.2.40", + "kotlin_compiler_embeddable": "org.jetbrains.kotlin:kotlin-compiler-embeddable:1.2.40", + "klob": "com.github.shyiko.klob:klob:0.2.0", + "aether_api": "org.eclipse.aether:aether-api:1.1.0", + "aether_spi": "org.eclipse.aether:aether-spi:1.1.0", + "aether_util": "org.eclipse.aether:aether-util:1.1.0", + "aether_impl": "org.eclipse.aether:aether-impl:1.1.0", + "aether_connector_basic": "org.eclipse.aether:aether-connector-basic:1.1.0", + "aether_transport_file": "org.eclipse.aether:aether-transport-file:1.1.0", + "aether_transport_http": "org.eclipse.aether:aether-transport-http:1.1.0", + // Used to silence aether-transport-http + "slf4j_nop": "org.slf4j:slf4j-nop:1.6.2", + "maven_aether_provider": "org.apache.maven:maven-aether-provider:3.2.5", + "picocli": "info.picocli:picocli:2.3.0", + "kolor": dependencies.create("com.andreapivetta.kolor:kolor:0.0.2") { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre8" + }, + + // Testing libraries + "testng": "org.testng:testng:6.8.21", + "assertj_core": "org.assertj:assertj-core:1.7.1", + "jimfs": "com.google.jimfs:jimfs:1.1" +] + +subprojects { + repositories { + jcenter() + mavenCentral() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 00000000..13372aef --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..fc449c78 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Jan 28 12:56:37 PST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ktlint-core/build.gradle b/ktlint-core/build.gradle new file mode 100644 index 00000000..3d45e08e --- /dev/null +++ b/ktlint-core/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile libraries.kotlin_stdlib + compile libraries.kotlin_compiler_embeddable + + testCompile libraries.testng + testCompile libraries.assertj_core +} diff --git a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/KtLint.kt b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/KtLint.kt index 13c7ea4e..977b97d7 100644 --- a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/KtLint.kt +++ b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/KtLint.kt @@ -1,10 +1,13 @@ package com.github.shyiko.ktlint.core +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.com.intellij.openapi.Disposable +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger import org.jetbrains.kotlin.com.intellij.openapi.extensions.ExtensionPoint import org.jetbrains.kotlin.com.intellij.openapi.extensions.Extensions.getArea import org.jetbrains.kotlin.com.intellij.openapi.util.Key @@ -28,18 +31,30 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset import sun.reflect.ReflectionFactory import java.util.ArrayList import java.util.HashSet +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger as DiagnosticLogger object KtLint { val EDITOR_CONFIG_USER_DATA_KEY = Key<EditorConfig>("EDITOR_CONFIG") val ANDROID_USER_DATA_KEY = Key<Boolean>("ANDROID") + val FILE_PATH_USER_DATA_KEY = Key<String>("FILE_PATH") private val psiFileFactory: PsiFileFactory private val nullSuppression = { _: Int, _: String -> false } init { + // do not print anything to the stderr when lexer is unable to match input + class LoggerFactory : DiagnosticLogger.Factory { + override fun getLoggerInstance(p: String): DiagnosticLogger = object : DefaultLogger(null) { + override fun warn(message: String?, t: Throwable?) {} + override fun error(message: String?, vararg details: String?) {} + } + } + DiagnosticLogger.setFactory(LoggerFactory::class.java) + val compilerConfiguration = CompilerConfiguration() + compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) val project = KotlinCoreEnvironment.createForProduction(Disposable {}, - CompilerConfiguration(), EnvironmentConfigFiles.EMPTY).project + compilerConfiguration, EnvironmentConfigFiles.JVM_CONFIG_FILES).project // everything below (up to PsiFileFactory.getInstance(...)) is to get AST mutations (`ktlint -F ...`) working // otherwise it's not needed val pomModel: PomModel = object : UserDataHolderBase(), PomModel { @@ -129,43 +144,100 @@ object KtLint { throw ParseException(line, col, errorElement.errorDescription) } val rootNode = psiFile.node - rootNode.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(userData - "android")) + rootNode.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(userData - "android" - "file_path")) rootNode.putUserData(ANDROID_USER_DATA_KEY, userData["android"]?.toBoolean() ?: false) + rootNode.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"]) val isSuppressed = calculateSuppressedRegions(rootNode) - val r = flatten(ruleSets) - rootNode.visit { node -> - r.forEach { (id, rule) -> - if (!isSuppressed(node.startOffset, id)) { - try { - rule.visit(node, false) { offset, errorMessage, _ -> - val (line, col) = positionByOffset(offset) - cb(LintError(line, col, id, errorMessage)) - } - } catch (e: Exception) { - val (line, col) = positionByOffset(node.startOffset) - throw RuleExecutionException(line, col, id, e) + visitor(rootNode, ruleSets).invoke { node, rule, fqRuleId -> + // fixme: enforcing suppression based on node.startOffset is wrong + // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) + if (!isSuppressed(node.startOffset, fqRuleId) || node === rootNode) { + try { + rule.visit(node, false) { offset, errorMessage, _ -> + val (line, col) = positionByOffset(offset) + cb(LintError(line, col, fqRuleId, errorMessage)) } + } catch (e: Exception) { + val (line, col) = positionByOffset(node.startOffset) + throw RuleExecutionException(line, col, fqRuleId, e) } } } } - private fun flatten(ruleSets: Iterable<RuleSet>) = ArrayList<Pair<String, Rule>>().apply { - ruleSets.forEach { ruleSet -> + private fun visitor( + rootNode: ASTNode, + ruleSets: Iterable<RuleSet>, + concurrent: Boolean = true, + filter: (fqRuleId: String) -> Boolean = { true } + ): ((node: ASTNode, rule: Rule, fqRuleId: String) -> Unit) -> Unit { + val fqrsRestrictedToRoot = mutableListOf<Pair<String, Rule>>() + val fqrs = mutableListOf<Pair<String, Rule>>() + val fqrsExpectedToBeExecutedLastOnRoot = mutableListOf<Pair<String, Rule>>() + val fqrsExpectedToBeExecutedLast = mutableListOf<Pair<String, Rule>>() + for (ruleSet in ruleSets) { val prefix = if (ruleSet.id === "standard") "" else "${ruleSet.id}:" - ruleSet.forEach { rule -> add("$prefix${rule.id}" to rule) } + for (rule in ruleSet) { + val fqRuleId = "$prefix${rule.id}" + if (!filter(fqRuleId)) { + continue + } + val fqr = fqRuleId to rule + when { + rule is Rule.Modifier.Last -> fqrsExpectedToBeExecutedLast.add(fqr) + rule is Rule.Modifier.RestrictToRootLast -> fqrsExpectedToBeExecutedLastOnRoot.add(fqr) + rule is Rule.Modifier.RestrictToRoot -> fqrsRestrictedToRoot.add(fqr) + else -> fqrs.add(fqr) + } + } + } + return { visit -> + for ((fqRuleId, rule) in fqrsRestrictedToRoot) { + visit(rootNode, rule, fqRuleId) + } + if (concurrent) { + rootNode.visit { node -> + for ((fqRuleId, rule) in fqrs) { + visit(node, rule, fqRuleId) + } + } + } else { + for ((fqRuleId, rule) in fqrs) { + rootNode.visit { node -> + visit(node, rule, fqRuleId) + } + } + } + for ((fqRuleId, rule) in fqrsExpectedToBeExecutedLastOnRoot) { + visit(rootNode, rule, fqRuleId) + } + if (!fqrsExpectedToBeExecutedLast.isEmpty()) { + if (concurrent) { + rootNode.visit { node -> + for ((fqRuleId, rule) in fqrsExpectedToBeExecutedLast) { + visit(node, rule, fqRuleId) + } + } + } else { + for ((fqRuleId, rule) in fqrsExpectedToBeExecutedLast) { + rootNode.visit { node -> + visit(node, rule, fqRuleId) + } + } + } + } } } private fun calculateLineColByOffset(text: String): (offset: Int) -> Pair<Int, Int> { - var i = 0 + var i = -1 val e = text.length val arr = ArrayList<Int>() do { - arr.add(i) - i = text.indexOf('\n', i) + 1 - } while (i != 0 && i != e) - arr.add(e) + arr.add(i + 1) + i = text.indexOf('\n', i + 1) + } while (i != -1) + arr.add(e + if (arr.last() == e) 1 else 0) val segmentTree = SegmentTree(arr.toTypedArray()) return { offset -> val line = segmentTree.indexOf(offset) @@ -199,8 +271,12 @@ object KtLint { fun format(text: String, ruleSets: Iterable<RuleSet>, cb: (e: LintError, corrected: Boolean) -> Unit): String = format(text, ruleSets, emptyMap<String, String>(), cb, script = false) - fun format(text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, - cb: (e: LintError, corrected: Boolean) -> Unit): String = format(text, ruleSets, userData, cb, script = false) + fun format( + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError, corrected: Boolean) -> Unit + ): String = format(text, ruleSets, userData, cb, script = false) /** * Fix style violations. @@ -215,8 +291,12 @@ object KtLint { fun formatScript(text: String, ruleSets: Iterable<RuleSet>, cb: (e: LintError, corrected: Boolean) -> Unit): String = format(text, ruleSets, emptyMap(), cb, script = true) - fun formatScript(text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, - cb: (e: LintError, corrected: Boolean) -> Unit): String = format(text, ruleSets, userData, cb, script = true) + fun formatScript( + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError, corrected: Boolean) -> Unit + ): String = format(text, ruleSets, userData, cb, script = true) private fun format( text: String, @@ -238,55 +318,57 @@ object KtLint { throw ParseException(line, col, errorElement.errorDescription) } val rootNode = psiFile.node - rootNode.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(userData - "android")) + rootNode.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(userData - "android" - "file_path")) rootNode.putUserData(ANDROID_USER_DATA_KEY, userData["android"]?.toBoolean() ?: false) + rootNode.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"]) var isSuppressed = calculateSuppressedRegions(rootNode) - val r = flatten(ruleSets) - var autoCorrect = false - rootNode.visit { node -> - r.forEach { (id, rule) -> - if (!isSuppressed(node.startOffset, id)) { + var tripped = false + var mutated = false + visitor(rootNode, ruleSets, concurrent = false) + .invoke { node, rule, fqRuleId -> + // fixme: enforcing suppression based on node.startOffset is wrong + // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) + if (!isSuppressed(node.startOffset, fqRuleId) || node === rootNode) { try { - rule.visit(node, false) { offset, errorMessage, canBeAutoCorrected -> + rule.visit(node, true) { _, _, canBeAutoCorrected -> + tripped = true if (canBeAutoCorrected) { - autoCorrect = true + mutated = true + if (isSuppressed !== nullSuppression) { + isSuppressed = calculateSuppressedRegions(rootNode) + } } - val (line, col) = positionByOffset(offset) - cb(LintError(line, col, id, errorMessage), canBeAutoCorrected) } } catch (e: Exception) { - val (line, col) = positionByOffset(node.startOffset) - throw RuleExecutionException(line, col, id, e) + // line/col cannot be reliably mapped as exception might originate from a node not present + // in the original AST + throw RuleExecutionException(0, 0, fqRuleId, e) } } } - } - if (autoCorrect) { - rootNode.visit { node -> - r.forEach { (id, rule) -> - if (!isSuppressed(node.startOffset, id)) { - try { - rule.visit(node, true) { _, _, canBeAutoCorrected -> - if (canBeAutoCorrected && isSuppressed !== nullSuppression) { - isSuppressed = calculateSuppressedRegions(rootNode) - } - } - } catch (e: Exception) { - // line/col cannot be reliably mapped as exception might originate from a node not present - // in the original AST - throw RuleExecutionException(0, 0, id, e) + if (tripped) { + visitor(rootNode, ruleSets).invoke { node, rule, fqRuleId -> + // fixme: enforcing suppression based on node.startOffset is wrong + // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) + if (!isSuppressed(node.startOffset, fqRuleId) || node === rootNode) { + try { + rule.visit(node, false) { offset, errorMessage, _ -> + val (line, col) = positionByOffset(offset) + cb(LintError(line, col, fqRuleId, errorMessage), false) } + } catch (e: Exception) { + val (line, col) = positionByOffset(node.startOffset) + throw RuleExecutionException(line, col, fqRuleId, e) } } } - return rootNode.text.replace("\n", determineLineSeparator(text)) } - return text + return if (mutated) rootNode.text.replace("\n", determineLineSeparator(text)) else text } private fun calculateLineBreakOffset(fileContent: String): (offset: Int) -> Int { val arr = ArrayList<Int>() - var i: Int = 0 + var i = 0 do { arr.add(i) i = fileContent.indexOf("\r\n", i + 1) @@ -296,13 +378,8 @@ object KtLint { SegmentTree(arr.toTypedArray()).let { return { offset -> it.indexOf(offset) } } else { _ -> 0 } } - private fun determineLineSeparator(fileContent: String): String { - val i = fileContent.lastIndexOf('\n') - if (i == -1) { - return if (fileContent.lastIndexOf('\r') == -1) System.getProperty("line.separator") else "\r" - } - return if (i != 0 && fileContent[i] == '\r') "\r\n" else "\n" - } + private fun determineLineSeparator(fileContent: String) = + if (fileContent.lastIndexOf('\r') != -1) "\r\n" else "\n" /** * @param range zero-based range of lines where lint errors should be suppressed @@ -329,8 +406,8 @@ object KtLint { val commentText = text.removePrefix("/*").removeSuffix("*/").trim() parseHintArgs(commentText, "ktlint-disable")?.apply { open.add(SuppressionHint(IntRange(node.startOffset, node.startOffset), HashSet(this))) - } ?: - parseHintArgs(commentText, "ktlint-enable")?.apply { + } + ?: parseHintArgs(commentText, "ktlint-enable")?.apply { // match open hint val disabledRules = HashSet(this) val openHintIndex = open.indexOfLast { it.disabledRules == disabledRules } @@ -363,7 +440,7 @@ object KtLint { private fun splitCommentBySpace(comment: String) = comment.replace(Regex("\\s"), " ").replace(" {2,}", " ").split(" ") - private fun <T>List<T>.tail() = this.subList(1, this.size) + private fun <T> List<T>.tail() = this.subList(1, this.size) } } diff --git a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/ReporterProvider.kt b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/ReporterProvider.kt index 1cfd3d84..6d7ef310 100644 --- a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/ReporterProvider.kt +++ b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/ReporterProvider.kt @@ -4,7 +4,7 @@ import java.io.PrintStream /** * `ktlint` uses [ServiceLoader](http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html) to - * discover all available `ReporterProvider`s on the classpath and so each `ReporterProvider` must be registered using + * discover all available [ReporterProvider]s on the classpath and so each [ReporterProvider] must be registered using * `META-INF/services/com.github.shyiko.ktlint.core.ReporterProvider` * (see `ktlint-reporter-plain/src/main/resources` for an example). */ diff --git a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/Rule.kt b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/Rule.kt index 33905631..20eddfd1 100644 --- a/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/Rule.kt +++ b/ktlint-core/src/main/kotlin/com/github/shyiko/ktlint/core/Rule.kt @@ -1,12 +1,13 @@ package com.github.shyiko.ktlint.core import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode /** * A rule contract. * * Implementation **doesn't** have to be thread-safe or stateless - * (provided RuleSetProvider creates a new instance on each `get()` call). + * (provided [RuleSetProvider] creates a new instance on each `get()` call). * * @param id must be unique within the ruleset * @@ -25,6 +26,25 @@ abstract class Rule(val id: String) { * @param autoCorrect indicates whether rule should attempt auto-correction * @param emit a way for rule to notify about a violation (lint error) */ - abstract fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) + abstract fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) + + object Modifier { + /** + * Any rule implementing this interface will be given root ([FileASTNode]) node only + * (in other words, [visit] will be called on [FileASTNode] but not on [FileASTNode] children). + */ + interface RestrictToRoot + /** + * Marker interface to indicate that rule must be executed after all other rules (order among multiple + * [RestrictToRootLast] rules is not defined and should be assumed to be random). + * + * Note that [RestrictToRootLast] implements [RestrictToRoot]. + */ + interface RestrictToRootLast : RestrictToRoot + interface Last + } } diff --git a/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/ErrorSuppressionTest.kt b/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/ErrorSuppressionTest.kt index c85dea7f..73542369 100644 --- a/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/ErrorSuppressionTest.kt +++ b/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/ErrorSuppressionTest.kt @@ -13,8 +13,11 @@ class ErrorSuppressionTest { @Test fun testErrorSuppression() { class NoWildcardImportsRule : Rule("no-wildcard-imports") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches("*") && PsiTreeUtil.getNonStrictParentOfType(node, KtImportDirective::class.java) != null) { emit(node.startOffset, "Wildcard import", false) diff --git a/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/KtLintTest.kt b/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/KtLintTest.kt new file mode 100644 index 00000000..b56bcb4e --- /dev/null +++ b/ktlint-core/src/test/kotlin/com/github/shyiko/ktlint/core/KtLintTest.kt @@ -0,0 +1,36 @@ +package com.github.shyiko.ktlint.core + +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes +import org.testng.annotations.Test + +class KtLintTest { + + @Test + fun testRuleExecutionOrder() { + open class R(private val bus: MutableList<String>, id: String) : Rule(id) { + private var done = false + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtStubElementTypes.FILE) { + bus.add("file:$id") + } else if (!done) { + bus.add(id) + done = true + } + } + } + val bus = mutableListOf<String>() + KtLint.lint("fun main() {}", listOf(RuleSet("standard", + object : R(bus, "d"), Rule.Modifier.RestrictToRootLast {}, + R(bus, "b"), + object : R(bus, "a"), Rule.Modifier.RestrictToRoot {}, + R(bus, "c") + ))) {} + assertThat(bus).isEqualTo(listOf("file:a", "file:b", "file:c", "b", "c", "file:d")) + } +} diff --git a/ktlint-reporter-checkstyle/build.gradle b/ktlint-reporter-checkstyle/build.gradle new file mode 100644 index 00000000..27b42f88 --- /dev/null +++ b/ktlint-reporter-checkstyle/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile project(":ktlint-core") + compile libraries.kotlin_stdlib + + testCompile libraries.testng + testCompile libraries.assertj_core +} diff --git a/ktlint-reporter-checkstyle/src/test/kotlin/com/github/shyiko/ktlint/reporter/checkstyle/CheckStyleReporterTest.kt b/ktlint-reporter-checkstyle/src/test/kotlin/com/github/shyiko/ktlint/reporter/checkstyle/CheckStyleReporterTest.kt index 9243d633..a6c09a86 100644 --- a/ktlint-reporter-checkstyle/src/test/kotlin/com/github/shyiko/ktlint/reporter/checkstyle/CheckStyleReporterTest.kt +++ b/ktlint-reporter-checkstyle/src/test/kotlin/com/github/shyiko/ktlint/reporter/checkstyle/CheckStyleReporterTest.kt @@ -37,7 +37,7 @@ class CheckStyleReporterTest { <error line="2" column="20" severity="error" message="A single thin straight line" source="rule-2" /> </file> </checkstyle> -""".trimStart() +""".trimStart().replace("\n", System.lineSeparator()) ) } } diff --git a/ktlint-reporter-json/build.gradle b/ktlint-reporter-json/build.gradle new file mode 100644 index 00000000..951dcf6e --- /dev/null +++ b/ktlint-reporter-json/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile project(':ktlint-core') + compile libraries.kotlin_stdlib + + testCompile libraries.testng + testCompile libraries.assertj_core +} diff --git a/ktlint-reporter-json/src/main/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporter.kt b/ktlint-reporter-json/src/main/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporter.kt index 3d9b2097..cb2ff302 100644 --- a/ktlint-reporter-json/src/main/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporter.kt +++ b/ktlint-reporter-json/src/main/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporter.kt @@ -18,32 +18,24 @@ class JsonReporter(val out: PrintStream) : Reporter { override fun afterAll() { out.println("[") - for ((i, entry) in acc.entries.sortedBy { it.key }.withIndex()) { + val indexLast = acc.size - 1 + for ((index, entry) in acc.entries.sortedBy { it.key }.withIndex()) { val (file, errList) = entry - out.println( - """ - | { - | "file": "${file.escapeJsonValue()}", - | "errors": [ - """.trimMargin() - ) - out.println( - errList.map { (line, col, ruleId, detail) -> - """ - | { - | "line": $line, - | "column": $col, - | "message": "${detail.escapeJsonValue()}", - | "rule": "$ruleId" - | } - """.trimMargin() - }.joinToString(",\n") - ) - out.println( - """ - | ] - | }${if (i < acc.size - 1) "," else ""} - """.trimMargin()) + out.println(""" {""") + out.println(""" "file": "${file.escapeJsonValue()}",""") + out.println(""" "errors": [""") + val errIndexLast = errList.size - 1 + for ((errIndex, err) in errList.withIndex()) { + val (line, col, ruleId, detail) = err + out.println(""" {""") + out.println(""" "line": $line,""") + out.println(""" "column": $col,""") + out.println(""" "message": "${detail.escapeJsonValue()}",""") + out.println(""" "rule": "$ruleId"""") + out.println(""" }${if (errIndex != errIndexLast) "," else ""}""") + } + out.println(""" ]""") + out.println(""" }${if (index != indexLast) "," else ""}""") } out.println("]") } diff --git a/ktlint-reporter-json/src/test/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporterTest.kt b/ktlint-reporter-json/src/test/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporterTest.kt index d3512e23..02ce58c5 100644 --- a/ktlint-reporter-json/src/test/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporterTest.kt +++ b/ktlint-reporter-json/src/test/kotlin/com/github/shyiko/ktlint/reporter/json/JsonReporterTest.kt @@ -57,7 +57,7 @@ class JsonReporterTest { ] } ] -""".trimStart() +""".trimStart().replace("\n", System.lineSeparator()) ) } } diff --git a/ktlint-reporter-plain/build.gradle b/ktlint-reporter-plain/build.gradle new file mode 100644 index 00000000..e512d044 --- /dev/null +++ b/ktlint-reporter-plain/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile project(':ktlint-core') + compile libraries.kotlin_stdlib + compile libraries.kolor + + testCompile libraries.testng + testCompile libraries.assertj_core +} diff --git a/ktlint-reporter-plain/pom.xml b/ktlint-reporter-plain/pom.xml index 3d79e4dc..8287fef7 100644 --- a/ktlint-reporter-plain/pom.xml +++ b/ktlint-reporter-plain/pom.xml @@ -25,6 +25,17 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>com.andreapivetta.kolor</groupId> + <artifactId>kolor</artifactId> + <version>${kolor.version}</version> + <exclusions> + <exclusion> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-stdlib-jre8</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version> diff --git a/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporter.kt b/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporter.kt index 0586cce1..72229501 100644 --- a/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporter.kt +++ b/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporter.kt @@ -1,12 +1,21 @@ package com.github.shyiko.ktlint.reporter.plain +import com.andreapivetta.kolor.Color +import com.andreapivetta.kolor.Kolor import com.github.shyiko.ktlint.core.LintError import com.github.shyiko.ktlint.core.Reporter +import java.io.File import java.io.PrintStream import java.util.ArrayList import java.util.concurrent.ConcurrentHashMap -class PlainReporter(val out: PrintStream, val verbose: Boolean = false, val groupByFile: Boolean = false) : Reporter { +class PlainReporter( + val out: PrintStream, + val verbose: Boolean = false, + val groupByFile: Boolean = false, + val color: Boolean = false, + val pad: Boolean = false +) : Reporter { private val acc = ConcurrentHashMap<String, MutableList<LintError>>() @@ -15,8 +24,9 @@ class PlainReporter(val out: PrintStream, val verbose: Boolean = false, val grou if (groupByFile) { acc.getOrPut(file) { ArrayList<LintError>() }.add(err) } else { - out.println("$file:${err.line}:${err.col}: " + - "${err.detail}${if (verbose) " (${err.ruleId})" else ""}") + out.println("${colorFileName(file)}${":".gray()}${err.line}${ + ":${"${err.col}:".let { if (pad) String.format("%-4s", it) else it}}".gray() + } ${err.detail}${if (verbose) " (${err.ruleId})".gray() else ""}") } } } @@ -24,11 +34,20 @@ class PlainReporter(val out: PrintStream, val verbose: Boolean = false, val grou override fun after(file: String) { if (groupByFile) { val errList = acc[file] ?: return - out.println(file) + out.println(colorFileName(file)) for ((line, col, ruleId, detail) in errList) { - out.println(" $line:$col " + - "$detail${if (verbose) " ($ruleId)" else ""}") + out.println(" $line${ + ":${if (pad) String.format("%-3s", col) else "$col"}".gray() + } $detail${if (verbose) " ($ruleId)".gray() else ""}") } } } + + private fun colorFileName(fileName: String): String { + val name = fileName.substringAfterLast(File.separator) + return fileName.substring(0, fileName.length - name.length).gray() + name + } + + private fun String.gray() = + if (color) Kolor.foreground(this, Color.DARK_GRAY) else this } diff --git a/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterProvider.kt b/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterProvider.kt index 9b254447..f4bb01f2 100644 --- a/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterProvider.kt +++ b/ktlint-reporter-plain/src/main/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterProvider.kt @@ -5,8 +5,17 @@ import com.github.shyiko.ktlint.core.ReporterProvider import java.io.PrintStream class PlainReporterProvider : ReporterProvider { + override val id: String = "plain" - override fun get(out: PrintStream, opt: Map<String, String>): Reporter = PlainReporter(out, - verbose = opt["verbose"]?.emptyOrTrue() ?: false, groupByFile = opt["group_by_file"]?.emptyOrTrue() ?: false) + + override fun get(out: PrintStream, opt: Map<String, String>): Reporter = + PlainReporter( + out, + verbose = opt["verbose"]?.emptyOrTrue() ?: false, + groupByFile = opt["group_by_file"]?.emptyOrTrue() ?: false, + color = opt["color"]?.emptyOrTrue() ?: false, + pad = opt["pad"]?.emptyOrTrue() ?: false + ) + private fun String.emptyOrTrue() = this == "" || this == "true" } diff --git a/ktlint-reporter-plain/src/test/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterTest.kt b/ktlint-reporter-plain/src/test/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterTest.kt index 61c896b3..10f5d7f5 100644 --- a/ktlint-reporter-plain/src/test/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterTest.kt +++ b/ktlint-reporter-plain/src/test/kotlin/com/github/shyiko/ktlint/reporter/plain/PlainReporterTest.kt @@ -29,7 +29,7 @@ class PlainReporterTest { /one-fixed-and-one-not.kt:1:1: <"&'> /two-not-fixed.kt:1:10: I thought I would again /two-not-fixed.kt:2:20: A single thin straight line -""".trimStart() +""".trimStart().replace("\n", System.lineSeparator()) ) } @@ -59,7 +59,7 @@ class PlainReporterTest { /two-not-fixed.kt 1:10 I thought I would again 2:20 A single thin straight line -""".trimStart() +""".trimStart().replace("\n", System.lineSeparator()) ) } } diff --git a/ktlint-ruleset-standard/build.gradle b/ktlint-ruleset-standard/build.gradle new file mode 100644 index 00000000..8d2ee519 --- /dev/null +++ b/ktlint-ruleset-standard/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile project(':ktlint-core') + compile project(':ktlint-test') + compile libraries.kotlin_stdlib + + testCompile libraries.testng + testCompile libraries.assertj_core +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt new file mode 100644 index 00000000..3c24344d --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt @@ -0,0 +1,127 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl +import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.lexer.KtTokens.ANDAND +import org.jetbrains.kotlin.lexer.KtTokens.DIV +import org.jetbrains.kotlin.lexer.KtTokens.DOT +import org.jetbrains.kotlin.lexer.KtTokens.ELVIS +import org.jetbrains.kotlin.lexer.KtTokens.MINUS +import org.jetbrains.kotlin.lexer.KtTokens.MUL +import org.jetbrains.kotlin.lexer.KtTokens.OROR +import org.jetbrains.kotlin.lexer.KtTokens.PERC +import org.jetbrains.kotlin.lexer.KtTokens.PLUS +import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS +import org.jetbrains.kotlin.psi.psiUtil.nextLeaf +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf + +class ChainWrappingRule : Rule("chain-wrapping") { + + private val sameLineTokens = TokenSet.create(MUL, DIV, PERC, ANDAND, OROR) + private val prefixTokens = TokenSet.create(PLUS, MINUS) + private val nextLineTokens = TokenSet.create(DOT, SAFE_ACCESS, ELVIS) + private val noSpaceAroundTokens = TokenSet.create(DOT, SAFE_ACCESS) + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + /* + org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement (DOT) | "." + org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) | "\n " + org.jetbrains.kotlin.psi.KtCallExpression (CALL_EXPRESSION) + */ + val elementType = node.elementType + if (nextLineTokens.contains(elementType)) { + if (node.psi.isPartOf(PsiComment::class)) { + return + } + val nextLeaf = node.psi.nextLeafIgnoringWhitespaceAndComments()?.prevLeaf(true) + if (nextLeaf is PsiWhiteSpaceImpl && nextLeaf.textContains('\n')) { + emit(node.startOffset, "Line must not end with \"${node.text}\"", true) + if (autoCorrect) { + // rewriting + // <prevLeaf><node="."><nextLeaf="\n"> to + // <prevLeaf><delete space if any><nextLeaf="\n"><node="."><space if needed> + // (or) + // <prevLeaf><node="."><spaceBeforeComment><comment><nextLeaf="\n"> to + // <prevLeaf><delete space if any><spaceBeforeComment><comment><nextLeaf="\n"><node="."><space if needed> + val prevLeaf = node.psi.prevLeaf(true) + if (prevLeaf is PsiWhiteSpaceImpl) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + if (!noSpaceAroundTokens.contains(elementType)) { + nextLeaf.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } + node.treeParent.removeChild(node) + nextLeaf.rawInsertAfterMe(node.psi as LeafPsiElement) + } + } + } else if (sameLineTokens.contains(elementType) || prefixTokens.contains(elementType)) { + if (node.psi.isPartOf(PsiComment::class)) { + return + } + val prevLeaf = node.psi.prevLeaf(true) + if ( + prevLeaf is PsiWhiteSpaceImpl && + prevLeaf.textContains('\n') && + // fn(*typedArray<...>()) case + (elementType != MUL || !prevLeaf.isPartOfSpread()) && + // unary +/- + (!prefixTokens.contains(elementType) || !node.isInPrefixPosition()) && + // LeafPsiElement->KtOperationReferenceExpression->KtPrefixExpression->KtWhenConditionWithExpression + !node.isPartOfWhenCondition() + ) { + emit(node.startOffset, "Line must not begin with \"${node.text}\"", true) + if (autoCorrect) { + // rewriting + // <insertionPoint><prevLeaf="\n"><node="&&"><nextLeaf=" "> to + // <insertionPoint><prevLeaf=" "><node="&&"><nextLeaf="\n"><delete node="&&"><delete nextLeaf=" "> + // (or) + // <insertionPoint><spaceBeforeComment><comment><prevLeaf="\n"><node="&&"><nextLeaf=" "> to + // <insertionPoint><space if needed><node="&&"><spaceBeforeComment><comment><prevLeaf="\n"><delete node="&&"><delete nextLeaf=" "> + val nextLeaf = node.psi.nextLeaf(true) + if (nextLeaf is PsiWhiteSpaceImpl) { + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + val insertionPoint = prevLeaf.prevLeafIgnoringWhitespaceAndComments() as LeafPsiElement + node.treeParent.removeChild(node) + insertionPoint.rawInsertAfterMe(node.psi as LeafPsiElement) + if (!noSpaceAroundTokens.contains(elementType)) { + insertionPoint.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } + } + } + } + } + + private fun PsiElement.isPartOfSpread() = + prevLeafIgnoringWhitespaceAndComments()?.let { leaf -> + val type = leaf.node.elementType + type == KtTokens.LPAR || + type == KtTokens.COMMA || + type == KtTokens.LBRACE || + type == KtTokens.ELSE_KEYWORD || + KtTokens.OPERATIONS.contains(type) + } == true + + private fun ASTNode.isInPrefixPosition() = + treeParent?.treeParent?.elementType == KtNodeTypes.PREFIX_EXPRESSION + + private fun ASTNode.isPartOfWhenCondition() = + treeParent?.treeParent?.treeParent?.elementType == KtNodeTypes.WHEN_CONDITION_EXPRESSION + + private fun PsiElement.nextLeafIgnoringWhitespaceAndComments() = + this.nextLeaf { it.node.elementType != KtTokens.WHITE_SPACE && !it.isPartOf(PsiComment::class) } + + private fun PsiElement.prevLeafIgnoringWhitespaceAndComments() = + this.prevLeaf { it.node.elementType != KtTokens.WHITE_SPACE && !it.isPartOf(PsiComment::class) } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt new file mode 100644 index 00000000..68ba62b0 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt @@ -0,0 +1,35 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil + +class CommentSpacingRule : Rule("comment-spacing") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is PsiComment && node is LeafPsiElement && node.getText().startsWith("//")) { + val prevLeaf = PsiTreeUtil.prevLeaf(node) + if (prevLeaf !is PsiWhiteSpace && prevLeaf is LeafPsiElement) { + emit(node.startOffset, "Missing space before //", true) + if (autoCorrect) { + node.rawInsertBeforeMe(PsiWhiteSpaceImpl(" ")) + } + } + val text = node.getText() + if (text.length != 2 && !text.startsWith("// ")) { + emit(node.startOffset, "Missing space after //", true) + if (autoCorrect) { + node.rawReplaceWithText("// " + text.removePrefix("//")) + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt new file mode 100644 index 00000000..64a56484 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt @@ -0,0 +1,40 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.KtLint +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode + +// http://editorconfig.org/ +internal data class EditorConfig( + val indentSize: Int, + val continuationIndentSize: Int, + val maxLineLength: Int, + val insertFinalNewline: Boolean? +) { + + companion object { + + private const val DEFAULT_INDENT = 4 + + // https://android.github.io/kotlin-guides/style.html#line-wrapping + private const val ANDROID_MAX_LINE_LENGTH = 100 + + fun from(node: FileASTNode): EditorConfig { + val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! + val indentSizeRaw = editorConfig.get("indent_size") + val indentSize = when { + indentSizeRaw?.toLowerCase() == "unset" -> -1 + else -> indentSizeRaw?.toIntOrNull() ?: DEFAULT_INDENT + } + val continuationIndentSizeRaw = editorConfig.get("continuation_indent_size") + val continuationIndentSize = when { + continuationIndentSizeRaw?.toLowerCase() == "unset" -> -1 + else -> continuationIndentSizeRaw?.toIntOrNull() ?: indentSize + } + val android = node.getUserData(KtLint.ANDROID_USER_DATA_KEY)!! + val maxLineLength = editorConfig.get("max_line_length")?.toIntOrNull() + ?: if (android) ANDROID_MAX_LINE_LENGTH else -1 + val insertFinalNewline = editorConfig.get("insert_final_newline")?.toBoolean() + return EditorConfig(indentSize, continuationIndentSize, maxLineLength, insertFinalNewline) + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt new file mode 100644 index 00000000..8c7169c8 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt @@ -0,0 +1,63 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.KtLint +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes +import java.nio.file.Paths + +/** + * If there is only one top level class/object/typealias in a given file, then its name should match the file's name. + */ +class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot { + + private val ignoreSet = setOf<IElementType>( + KtStubElementTypes.FILE_ANNOTATION_LIST, + KtStubElementTypes.PACKAGE_DIRECTIVE, + KtStubElementTypes.IMPORT_LIST, + KtTokens.WHITE_SPACE, + KtTokens.EOL_COMMENT, + KtTokens.BLOCK_COMMENT, + KtTokens.DOC_COMMENT, + KtTokens.SHEBANG_COMMENT + ) + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) + if (filePath?.endsWith(".kt") != true) { + // ignore all non ".kt" files (including ".kts") + return + } + var type: String? = null + var className: String? = null + for (el in node.getChildren(null)) { + if (el.elementType == KtStubElementTypes.CLASS || + el.elementType == KtStubElementTypes.OBJECT_DECLARATION || + el.elementType == KtStubElementTypes.TYPEALIAS) { + if (className != null) { + // more than one class/object/typealias present + return + } + val id = el.findChildByType(KtTokens.IDENTIFIER) + type = id?.psi?.getPrevSiblingIgnoringWhitespaceAndComments(false)?.text + className = id?.text + } else if (!ignoreSet.contains(el.elementType)) { + // https://github.com/android/android-ktx/blob/master/src/main/java/androidx/core/graphics/Path.kt case + return + } + } + if (className != null) { + val name = Paths.get(filePath).fileName.toString().substringBefore(".") + if (name != "package" && name != className) { + emit(0, "$type $className should be declared in a file named $className.kt", false) + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt index 67057e6e..93e80290 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt @@ -1,13 +1,13 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -class FinalNewlineRule : Rule("final-newline") { +class FinalNewlineRule : Rule("final-newline"), Rule.Modifier.RestrictToRoot { override fun visit( node: ASTNode, @@ -15,9 +15,9 @@ class FinalNewlineRule : Rule("final-newline") { emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val insertFinalNewline = editorConfig.get("insert_final_newline")?.toBoolean() ?: return - val lastNode = node.lastChildNode + val ec = EditorConfig.from(node as FileASTNode) + val insertFinalNewline = ec.insertFinalNewline ?: return + val lastNode = lastChildNodeOf(node) if (insertFinalNewline) { if (lastNode !is PsiWhiteSpace || !lastNode.textContains('\n')) { // (PsiTreeUtil.getDeepestLast(lastNode.psi).node ?: lastNode).startOffset @@ -36,4 +36,7 @@ class FinalNewlineRule : Rule("final-newline") { } } } + + private tailrec fun lastChildNodeOf(node: ASTNode): ASTNode? = + if (node.lastChildNode == null) node else lastChildNodeOf(node.lastChildNode) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt index 87b833af..d270735d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt @@ -1,68 +1,74 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace -import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil -import org.jetbrains.kotlin.diagnostics.DiagnosticUtils -import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtParameterList -import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.KtTypeConstraintList import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class IndentationRule : Rule("indent") { - companion object { - // indentation size recommended by JetBrains - private const val DEFAULT_INDENT = 4 - } - - private var indent = DEFAULT_INDENT + private var indentSize = -1 - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val indentSize = editorConfig.get("indent_size") - indent = indentSize?.toIntOrNull() ?: if (indentSize?.toLowerCase() == "unset") -1 else indent + val ec = EditorConfig.from(node as FileASTNode) + indentSize = gcd(maxOf(ec.indentSize, 1), maxOf(ec.continuationIndentSize, 1)) return } - if (indent <= 0) { + if (indentSize <= 1) { return } - if (node is PsiWhiteSpace && !node.isPartOf(PsiComment::class)) { + if (node is PsiWhiteSpace) { val lines = node.getText().split("\n") - if (lines.size > 1) { + if (lines.size > 1 && !node.isPartOf(PsiComment::class) && !node.isPartOf(KtTypeConstraintList::class)) { var offset = node.startOffset + lines.first().length + 1 - val firstParameterColumn = lazy { - val firstParameter = PsiTreeUtil.findChildOfType( - node.getNonStrictParentOfType(KtParameterList::class.java), - KtParameter::class.java - ) - firstParameter?.run { - DiagnosticUtils.getLineAndColumnInPsiFile(node.containingFile, - TextRange(startOffset, startOffset)).column - } ?: 0 - } - lines.tail().forEach { line -> - if (line.length % indent != 0) { - if (node.isPartOf(KtParameterList::class) && firstParameterColumn.value != 0) { - if (firstParameterColumn.value - 1 != line.length) { - emit(offset, "Unexpected indentation (${line.length}) (" + - "parameters should be either vertically aligned or indented by the multiple of $indent" + - ")", false) - } - } else { - emit(offset, "Unexpected indentation (${line.length}) (it should be multiple of $indent)", false) + val previousIndentSize = node.previousIndentSize() + lines.tail().forEach { indent -> + if (indent.isNotEmpty() && (indent.length - previousIndentSize) % indentSize != 0) { + if (!node.isPartOf(KtParameterList::class)) { // parameter list wrapping enforced by ParameterListWrappingRule + emit( + offset, + "Unexpected indentation (${indent.length}) (it should be ${previousIndentSize + indentSize})", + false + ) } } - offset += line.length + 1 + offset += indent.length + 1 } } } } + + private fun gcd(a: Int, b: Int): Int = when { + a > b -> gcd(a - b, b) + a < b -> gcd(a, b - a) + else -> a + } + + // todo: calculating indent based on the previous line value is wrong (see IndentationRule.testLint) + private fun ASTNode.previousIndentSize(): Int { + var node = this.treeParent?.psi + while (node != null) { + val nextNode = node.nextSibling?.node?.elementType + if (node is PsiWhiteSpace && + nextNode != KtStubElementTypes.TYPE_REFERENCE && + nextNode != KtStubElementTypes.SUPER_TYPE_LIST && + nextNode != KtNodeTypes.CONSTRUCTOR_DELEGATION_CALL && + node.textContains('\n') && + node.nextLeaf()?.isPartOf(PsiComment::class) != true) { + return node.text.length - node.text.lastIndexOf('\n') - 1 + } + node = node.prevSibling ?: node.parent + } + return 0 + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt index 58d784e0..aa7cca16 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt @@ -1,16 +1,21 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.kdoc.psi.api.KDoc import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -class MaxLineLengthRule : Rule("max-line-length") { +class MaxLineLengthRule : Rule("max-line-length"), Rule.Modifier.Last { + + private var maxLineLength: Int = -1 + private var rangeTree = RangeTree() override fun visit( node: ASTNode, @@ -18,31 +23,134 @@ class MaxLineLengthRule : Rule("max-line-length") { emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val maxLineLength = editorConfig.get("max_line_length")?.toIntOrNull() ?: 0 + val ec = EditorConfig.from(node as FileASTNode) + maxLineLength = ec.maxLineLength if (maxLineLength <= 0) { return } + val errorOffset = arrayListOf<Int>() val text = node.text val lines = text.split("\n") var offset = 0 for (line in lines) { if (line.length > maxLineLength) { val el = node.psi.findElementAt(offset + line.length - 1)!! - if (!el.isPartOf(PsiComment::class)) { - if (!el.isPartOf(KtPackageDirective::class) && !el.isPartOf(KtImportDirective::class)) { - emit(offset, "Exceeded max line length ($maxLineLength)", false) - } - } else { - // if comment is the only thing on the line - fine, otherwise emit an error - val prevLeaf = el.getPrevSiblingIgnoringWhitespaceAndComments(false) - if (prevLeaf != null && prevLeaf.startOffset >= offset) { - emit(offset, "Exceeded max line length ($maxLineLength)", false) + if (!el.isPartOf(KDoc::class)) { + if (!el.isPartOf(PsiComment::class)) { + if (!el.isPartOf(KtPackageDirective::class) && !el.isPartOf(KtImportDirective::class)) { + // fixme: + // normally we would emit here but due to API limitations we need to hold off until + // node spanning the same offset is 'visit'ed + // (for ktlint-disable directive to have effect (when applied)) + // this will be rectified in the upcoming release(s) + errorOffset.add(offset) + } + } else { + // if comment is the only thing on the line - fine, otherwise emit an error + val prevLeaf = el.getPrevSiblingIgnoringWhitespaceAndComments(false) + if (prevLeaf != null && prevLeaf.startOffset >= offset) { + // fixme: + // normally we would emit here but due to API limitations we need to hold off until + // node spanning the same offset is 'visit'ed + // (for ktlint-disable directive to have effect (when applied)) + // this will be rectified in the upcoming release(s) + errorOffset.add(offset) + } } } } offset += line.length + 1 } + rangeTree = RangeTree(errorOffset) + } else if (!rangeTree.isEmpty() && node.psi is LeafPsiElement) { + rangeTree + .query(node.startOffset, node.startOffset + node.textLength) + .forEach { offset -> + emit(offset, "Exceeded max line length ($maxLineLength)", false) + } + } + } +} + +class RangeTree(seq: List<Int> = emptyList()) { + + private var emptyArrayView = ArrayView(0, 0) + private var arr: IntArray = seq.toIntArray() + + init { + if (arr.isNotEmpty()) { + arr.reduce { p, n -> require(p <= n) { "Input must be sorted" }; n } + } + } + + // runtime: O(log(n)+k), where k is number of matching points + // space: O(1) + fun query(vmin: Int, vmax: Int): ArrayView { + var r = arr.size - 1 + if (r == -1 || vmax < arr[0] || arr[r] < vmin) { + return emptyArrayView + } + // binary search for min(arr[l] >= vmin) + var l = 0 + while (l < r) { + val m = (r + l) / 2 + if (vmax < arr[m]) { + r = m - 1 + } else if (arr[m] < vmin) { + l = m + 1 + } else { + // arr[l] ?<=? vmin <= arr[m] <= vmax ?<=? arr[r] + if (vmin <= arr[l]) break else l++ // optimization + r = m + } + } + if (l > r || arr[l] < vmin) { + return emptyArrayView + } + // find max(k) such as arr[k] < vmax + var k = l + while (k < arr.size) { + if (arr[k] >= vmax) { + break + } + k++ + } + return ArrayView(l, k) + } + + fun isEmpty() = arr.isEmpty() + + inner class ArrayView(private var l: Int, private val r: Int) { + + val size: Int = r - l + + fun get(i: Int): Int { + if (i < 0 || i >= size) { + throw IndexOutOfBoundsException() + } + return arr[l + i] + } + + inline fun forEach(cb: (v: Int) -> Unit) { + var i = 0 + while (i < size) { + cb(get(i++)) + } + } + + override fun toString(): String { + if (l == r) { + return "[]" + } + val sb = StringBuilder("[") + var i = l + while (i < r) { + sb.append(arr[i]).append(", ") + i++ + } + sb.replace(sb.length - 2, sb.length, "") + sb.append("]") + return sb.toString() } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt index 4815c065..3c9c9e5d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt @@ -2,14 +2,15 @@ package com.github.shyiko.ktlint.ruleset.standard import com.github.shyiko.ktlint.core.Rule 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.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ACTUAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ANNOTATION_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.COMPANION_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.DATA_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ENUM_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXPECT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EXTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.FINAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.INFIX_KEYWORD @@ -26,25 +27,34 @@ import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SEALED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SUSPEND_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.TAILREC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VARARG_KEYWORD +import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtDeclarationModifierList +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.ANNOTATION_ENTRY import java.util.Arrays class ModifierOrderRule : Rule("modifier-order") { - // subset of KtTokens.MODIFIER_KEYWORDS_ARRAY + // subset of KtTokens.MODIFIER_KEYWORDS_ARRAY (+ annotations entries) private val order = arrayOf( + ANNOTATION_ENTRY, PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - FINAL_KEYWORD, OPEN_KEYWORD, ABSTRACT_KEYWORD, - SUSPEND_KEYWORD, TAILREC_KEYWORD, + EXPECT_KEYWORD, ACTUAL_KEYWORD, + FINAL_KEYWORD, OPEN_KEYWORD, ABSTRACT_KEYWORD, SEALED_KEYWORD, CONST_KEYWORD, + EXTERNAL_KEYWORD, OVERRIDE_KEYWORD, - CONST_KEYWORD, LATEINIT_KEYWORD, - INNER_KEYWORD, EXTERNAL_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, DATA_KEYWORD, + LATEINIT_KEYWORD, + TAILREC_KEYWORD, + VARARG_KEYWORD, + SUSPEND_KEYWORD, + INNER_KEYWORD, + ENUM_KEYWORD, ANNOTATION_KEYWORD, COMPANION_KEYWORD, INLINE_KEYWORD, - // NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, OUT_KEYWORD, IN_KEYWORD, VARARG_KEYWORD, REIFIED_KEYWORD INFIX_KEYWORD, - OPERATOR_KEYWORD + OPERATOR_KEYWORD, + DATA_KEYWORD + // NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, OUT_KEYWORD, IN_KEYWORD, REIFIED_KEYWORD // HEADER_KEYWORD, IMPL_KEYWORD ) private val tokenSet = TokenSet.create(*order) @@ -58,16 +68,26 @@ class ModifierOrderRule : Rule("modifier-order") { val modifierArr = node.getChildren(tokenSet) val sorted = modifierArr.copyOf().apply { sortWith(compareBy { order.indexOf(it.elementType) }) } if (!Arrays.equals(modifierArr, sorted)) { + // Since annotations can be fairly lengthy and/or span multiple lines we are + // squashing them into a single placeholder text to guarantee a single line output emit(node.startOffset, "Incorrect modifier order (should be \"${ - sorted.map { it.text }.joinToString(" ") + squashAnnotations(sorted).joinToString(" ") }\")", true) if (autoCorrect) { modifierArr.forEachIndexed { i, n -> - // fixme: find a better way (node type is now potentially out of sync) - (n.psi as LeafPsiElement).replaceWithText(sorted[i].text) + node.replaceChild(n, sorted[i].clone() as ASTNode) } } } } } + + private fun squashAnnotations(sorted: Array<ASTNode>): List<String> { + val nonAnnotationModifiers = sorted.filter { it.psi !is KtAnnotationEntry } + return if (nonAnnotationModifiers.size != sorted.size) { + listOf("@Annotation...") + nonAnnotationModifiers.map { it.text } + } else { + nonAnnotationModifiers.map { it.text } + } + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt index ce64489f..cd7649e1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt @@ -9,17 +9,20 @@ import org.jetbrains.kotlin.lexer.KtTokens class NoBlankLineBeforeRbraceRule : Rule("no-blank-line-before-rbrace") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace && node.textContains('\n') && PsiTreeUtil.nextLeaf(node, true)?.node?.elementType == KtTokens.RBRACE) { val split = node.getText().split("\n") if (split.size > 2) { emit(node.startOffset + split[0].length + split[1].length + 1, - "Needless blank line(s)", true) + "Unexpected blank line(s) before \"}\"", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("${split.first()}\n${split.last()}") + (node as LeafPsiElement).rawReplaceWithText("${split.first()}\n${split.last()}") } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt index 1e6142f8..ae7c582d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt @@ -4,17 +4,22 @@ import com.github.shyiko.ktlint.core.Rule import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil class NoConsecutiveBlankLinesRule : Rule("no-consecutive-blank-lines") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace) { val split = node.getText().split("\n") - if (split.size > 3) { + if (split.size > 3 || split.size == 3 && PsiTreeUtil.nextLeaf(node) == null /* eof */) { emit(node.startOffset + split[0].length + split[1].length + 2, "Needless blank line(s)", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("${split.first()}\n\n${split.last()}") + (node as LeafPsiElement) + .rawReplaceWithText("${split.first()}\n${if (split.size > 3) "\n" else ""}${split.last()}") } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt index 59a57a34..5e412204 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt @@ -25,7 +25,7 @@ class NoEmptyClassBodyRule : Rule("no-empty-class-body") { if (autoCorrect) { val prevNode = node.psi.prevSibling.node val nextNode = PsiTreeUtil.nextLeaf(node.psi, true)?.node - if (prevNode.elementType == KtTokens.WHITE_SPACE && nextNode?.elementType == KtTokens.WHITE_SPACE) { + if (prevNode.elementType == KtTokens.WHITE_SPACE && (nextNode == null || nextNode.elementType == KtTokens.WHITE_SPACE)) { // remove space between declaration and block prevNode.treeParent.removeChild(prevNode) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt new file mode 100644 index 00000000..d28ba5eb --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt @@ -0,0 +1,28 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens + +class NoLineBreakAfterElseRule : Rule("no-line-break-after-else") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is PsiWhiteSpace && + node.textContains('\n')) { + if (PsiTreeUtil.prevLeaf(node, true)?.node?.elementType == KtTokens.ELSE_KEYWORD && + PsiTreeUtil.nextLeaf(node, true)?.node?.elementType.let { it == KtTokens.IF_KEYWORD || it == KtTokens.LBRACE }) { + emit(node.startOffset + 1, "Unexpected line break after \"else\"", true) + if (autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText(" ") + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt new file mode 100644 index 00000000..effbd187 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt @@ -0,0 +1,23 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.lexer.KtTokens + +class NoLineBreakBeforeAssignmentRule : Rule("no-line-break-before-assignment") { + + override fun visit(node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + if (node.elementType == KtTokens.EQ) { + val prevElement = node.treePrev?.psi + if (prevElement is PsiWhiteSpace && prevElement.text.contains("\n")) { + emit(node.startOffset, "Line break before assignment is not allowed", true) + if (autoCorrect) { + (node.treeNext?.psi as LeafPsiElement).rawReplaceWithText(prevElement.text) + (prevElement as LeafPsiElement).rawReplaceWithText(" ") + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt index 36cf26fe..15aa1a2a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt @@ -33,7 +33,8 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { val psi = node.psi if (psi is PsiComment) { comments.add(psi) } } - return comments.foldIndexed(mutableMapOf<Offset, CommentRelativeLocation>()) { i, acc, comment -> + return comments.foldIndexed(mutableMapOf()) { i, acc, comment -> + // todo: get rid of DiagnosticUtils (IndexOutOfBoundsException) val pos = DiagnosticUtils.getLineAndColumnInPsiFile(fileNode.psi as PsiFile, TextRange(comment.startOffset, comment.startOffset)) acc.put(comment.startOffset, CommentRelativeLocation( @@ -46,12 +47,14 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { } } - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.FILE) { fileNode = node - } else - if (node is PsiWhiteSpace && !node.textContains('\n') && node.getTextLength() > 1) { + } else if (node is PsiWhiteSpace && !node.textContains('\n') && node.getTextLength() > 1) { val nextLeaf = PsiTreeUtil.nextLeaf(node, true) if (nextLeaf is PsiComment) { val positionMap = commentMap @@ -71,13 +74,8 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { } emit(node.startOffset + 1, "Unnecessary space(s)", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText(" ") + (node as LeafPsiElement).rawReplaceWithText(" ") } } } - - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt index fce5ce3c..770e4090 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt @@ -10,8 +10,11 @@ import org.jetbrains.kotlin.psi.KtEnumEntry class NoSemicolonsRule : Rule("no-semi") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches(";") && !node.isPartOfString() && !node.isPartOf(KtEnumEntry::class)) { val nextLeaf = PsiTreeUtil.nextLeaf(node, true) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt index fa3e492f..a7e03c88 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt @@ -8,33 +8,41 @@ import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil class NoTrailingSpacesRule : Rule("no-trailing-spaces") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace) { val lines = node.getText().split("\n") if (lines.size > 1) { - checkForTrailingSpaces(lines.head(), node.startOffset, emit) - if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("\n".repeat(lines.size - 1) + lines.last()) + val violated = checkForTrailingSpaces(lines.head(), node.startOffset, emit) + if (violated && autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText("\n".repeat(lines.size - 1) + lines.last()) } - } else - if (PsiTreeUtil.nextLeaf(node) == null /* eof */) { - checkForTrailingSpaces(lines, node.startOffset, emit) - if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("\n".repeat(lines.size - 1)) + } else if (PsiTreeUtil.nextLeaf(node) == null /* eof */) { + val violated = checkForTrailingSpaces(lines, node.startOffset, emit) + if (violated && autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText("\n".repeat(lines.size - 1)) } } } } - private fun checkForTrailingSpaces(lines: List<String>, offset: Int, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + private fun checkForTrailingSpaces( + lines: List<String>, + offset: Int, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ): Boolean { + var violated = false var violationOffset = offset - return lines.forEach { line -> + lines.forEach { line -> if (!line.isEmpty()) { emit(violationOffset, "Trailing space(s)", true) + violated = true } violationOffset += line.length + 1 } + return violated } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt index 205448a3..c911cbbd 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt @@ -13,10 +13,10 @@ class NoUnitReturnRule : Rule("no-unit-return") { autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.elementType == KtStubElementTypes.TYPE_REFERENCE - && node.treeParent.elementType == KtStubElementTypes.FUNCTION - && node.text.contentEquals("Unit") - && PsiTreeUtil.nextVisibleLeaf(node.psi)?.node?.elementType == KtTokens.LBRACE) { + if (node.elementType == KtStubElementTypes.TYPE_REFERENCE && + node.treeParent.elementType == KtStubElementTypes.FUNCTION && + node.text.contentEquals("Unit") && + PsiTreeUtil.nextVisibleLeaf(node.psi)?.node?.elementType == KtTokens.LBRACE) { emit(node.startOffset, "Unnecessary \"Unit\" return type", true) if (autoCorrect) { var prevNode = node diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt index cbb8287d..b46f04df 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt @@ -12,6 +12,8 @@ import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class NoUnusedImportsRule : Rule("no-unused-imports") { + private val componentNRegex = Regex("^component\\d+$") + private val operatorSet = setOf( // unary "unaryPlus", "unaryMinus", "not", @@ -34,46 +36,45 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { // iteration (https://github.com/shyiko/ktlint/issues/40) "iterator", // by (https://github.com/shyiko/ktlint/issues/54) - "getValue", "setValue", - // destructuring assignment - "component1", "component2", "component3", "component4", "component5" + "getValue", "setValue" ) - private val ref = mutableSetOf("*") + private val ref = mutableSetOf<String>() private var packageName = "" - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.FILE) { + ref.clear() // rule can potentially be executed more than once (when formatting) + ref.add("*") node.visit { vnode -> val psi = vnode.psi val type = vnode.elementType if (type == KDocTokens.MARKDOWN_LINK && psi is KDocLink) { val linkText = psi.getLinkText().replace("`", "") ref.add(linkText.split('.').first()) - } else - if ((type == KtNodeTypes.REFERENCE_EXPRESSION || type == KtNodeTypes.OPERATION_REFERENCE) && + } else if ((type == KtNodeTypes.REFERENCE_EXPRESSION || type == KtNodeTypes.OPERATION_REFERENCE) && !psi.isPartOf(KtImportDirective::class)) { ref.add(vnode.text.trim('`')) } } - } else - if (node.elementType == KtStubElementTypes.PACKAGE_DIRECTIVE) { + } else if (node.elementType == KtStubElementTypes.PACKAGE_DIRECTIVE) { val packageDirective = node.psi as KtPackageDirective packageName = packageDirective.qualifiedName - } else - if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { + } else if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective val name = importDirective.importPath?.importedName?.asString() val importPath = importDirective.importPath?.pathStr!! if (importDirective.aliasName == null && - importPath.startsWith(packageName) && + (packageName.isEmpty() || importPath.startsWith("$packageName.")) && importPath.substring(packageName.length + 1).indexOf('.') == -1) { emit(importDirective.startOffset, "Unnecessary import", true) if (autoCorrect) { importDirective.delete() } - } else - if (name != null && !ref.contains(name) && !operatorSet.contains(name)) { + } else if (name != null && !ref.contains(name) && !operatorSet.contains(name) && !name.isComponentN()) { emit(importDirective.startOffset, "Unused import", true) if (autoCorrect) { importDirective.delete() @@ -82,8 +83,5 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { } } - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } + private fun String.isComponentN() = componentNRegex.matches(this) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt index db4e7bb9..9cb18aa1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt @@ -7,8 +7,11 @@ import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class NoWildcardImportsRule : Rule("no-wildcard-imports") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective val path = importDirective.importPath?.pathStr diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt new file mode 100644 index 00000000..81d06038 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt @@ -0,0 +1,151 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +class ParameterListWrappingRule : Rule("parameter-list-wrapping") { + + private var indentSize = -1 + private var maxLineLength = -1 + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtStubElementTypes.FILE) { + val ec = EditorConfig.from(node as FileASTNode) + indentSize = ec.indentSize + maxLineLength = ec.maxLineLength + return + } + if (indentSize <= 0) { + return + } + if (node.elementType == KtStubElementTypes.VALUE_PARAMETER_LIST && + // skip lambda parameters + node.treeParent?.elementType != KtNodeTypes.FUNCTION_LITERAL) { + // each parameter should be on a separate line if + // - at least one of the parameters is + // - maxLineLength exceeded (and separating parameters with \n would actually help) + // in addition, "(" and ")" must be on separates line if any of the parameters are (otherwise on the same) + val putParametersOnSeparateLines = node.textContains('\n') || + // max_line_length exceeded + maxLineLength > -1 && (node.psi.column - 1 + node.textLength) > maxLineLength + if (putParametersOnSeparateLines) { + // aiming for + // ... LPAR + // <line indent + indentSize> VALUE_PARAMETER... + // <line indent> RPAR + val indent = "\n" + node.psi.lineIndent() + val paramIndent = indent + " ".repeat(indentSize) // single indent as recommended by Jetbrains/Google + nextChild@ for (child in node.children()) { + when (child.elementType) { + KtTokens.LPAR -> { + val prevLeaf = child.psi.prevLeaf() + if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + prevLeaf.delete() + } + } + } + KtStubElementTypes.VALUE_PARAMETER, + KtTokens.RPAR -> { + var paramInnerIndentAdjustment = 0 + val prevLeaf = child.psi.prevLeaf() + val intendedIndent = if (child.elementType == KtStubElementTypes.VALUE_PARAMETER) + paramIndent else indent + if (prevLeaf is PsiWhiteSpace) { + val spacing = prevLeaf.text + val cut = spacing.lastIndexOf("\n") + if (cut > -1) { + val childIndent = spacing.substring(cut) + if (childIndent == intendedIndent) { + continue@nextChild + } + emit(child.startOffset, "Unexpected indentation" + + " (expected ${intendedIndent.length - 1}, actual ${childIndent.length - 1})", true) + } else { + emit(child.startOffset, errorMessage(child), true) + } + if (autoCorrect) { + val adjustedIndent = (if (cut > -1) spacing.substring(0, cut) else "") + intendedIndent + paramInnerIndentAdjustment = adjustedIndent.length - prevLeaf.textLength + (prevLeaf as LeafPsiElement).rawReplaceWithText(adjustedIndent) + } + } else { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + paramInnerIndentAdjustment = intendedIndent.length - child.psi.column + node.addChild(PsiWhiteSpaceImpl(intendedIndent), child) + } + } + if (paramInnerIndentAdjustment != 0 && + child.elementType == KtStubElementTypes.VALUE_PARAMETER) { + child.visit { n -> + if (n.elementType == KtTokens.WHITE_SPACE && n.textContains('\n')) { + val split = n.text.split("\n") + (n.psi as LeafElement).rawReplaceWithText(split.joinToString("\n") { + if (paramInnerIndentAdjustment > 0) { + it + " ".repeat(paramInnerIndentAdjustment) + } else { + it.substring(0, Math.max(it.length + paramInnerIndentAdjustment, 0)) + } + }) + } + } + } + } + } + } + } + } + } + + private val PsiElement.column: Int + get() { + var leaf = PsiTreeUtil.prevLeaf(this) + var offsetToTheLeft = 0 + while (leaf != null) { + if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) { + offsetToTheLeft += leaf.textLength - 1 - leaf.text.lastIndexOf('\n') + break + } + offsetToTheLeft += leaf.textLength + leaf = PsiTreeUtil.prevLeaf(leaf) + } + return offsetToTheLeft + 1 + } + + private fun errorMessage(node: ASTNode) = + when (node.elementType) { + KtTokens.LPAR -> """Unnecessary newline before "("""" + KtStubElementTypes.VALUE_PARAMETER -> + "Parameter should be on a separate line (unless all parameters can fit a single line)" + KtTokens.RPAR -> """Missing newline before ")"""" + else -> throw UnsupportedOperationException() + } + + private fun PsiElement.lineIndent(): String { + var leaf = PsiTreeUtil.prevLeaf(this) + while (leaf != null) { + if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) { + return leaf.text.substring(leaf.text.lastIndexOf('\n') + 1) + } + leaf = PsiTreeUtil.prevLeaf(leaf) + } + return "" + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt index bb9aca83..e2d6b6a7 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt @@ -14,8 +14,11 @@ import org.jetbrains.kotlin.psi.KtTypeParameterList class SpacingAroundColonRule : Rule("colon-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches(":") && !node.isPartOfString()) { if (node.isPartOf(KtAnnotation::class) || node.isPartOf(KtAnnotationEntry::class)) { // todo: enfore "no spacing" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt index 2ebca751..2be2935a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt @@ -6,16 +6,28 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.psi.psiUtil.startOffset class SpacingAroundCommaRule : Rule("comma-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { - if (node is LeafPsiElement && node.textMatches(",") && !node.isPartOfString() && - PsiTreeUtil.nextLeaf(node) !is PsiWhiteSpace) { - emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true) - if (autoCorrect) { - node.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is LeafPsiElement && node.textMatches(",") && !node.isPartOfString()) { + val prevLeaf = PsiTreeUtil.prevLeaf(node, true) + if (prevLeaf is PsiWhiteSpace) { + emit(prevLeaf.startOffset, "Unexpected spacing before \"${node.text}\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + } + if (PsiTreeUtil.nextLeaf(node) !is PsiWhiteSpace) { + emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true) + if (autoCorrect) { + node.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt index 2f6d226b..2bce695e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt @@ -13,20 +13,25 @@ import org.jetbrains.kotlin.psi.KtLambdaExpression class SpacingAroundCurlyRule : Rule("curly-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && !node.isPartOfString()) { val prevLeaf = PsiTreeUtil.prevLeaf(node, true) val nextLeaf = PsiTreeUtil.nextLeaf(node, true) val spacingBefore: Boolean val spacingAfter: Boolean if (node.textMatches("{")) { - spacingBefore = prevLeaf is PsiWhiteSpace || (prevLeaf?.node?.elementType == KtTokens.LPAR && + spacingBefore = prevLeaf is PsiWhiteSpace || prevLeaf?.node?.elementType == KtTokens.AT || (prevLeaf?.node?.elementType == KtTokens.LPAR && (node.parent is KtLambdaExpression || node.parent.parent is KtLambdaExpression)) spacingAfter = nextLeaf is PsiWhiteSpace || nextLeaf?.node?.elementType == KtTokens.RBRACE if (prevLeaf is PsiWhiteSpace && - !prevLeaf.textContains('\n') && - PsiTreeUtil.prevLeaf(prevLeaf, true)?.node?.elementType == KtTokens.LPAR) { + !prevLeaf.textContains('\n') && + PsiTreeUtil.prevLeaf(prevLeaf, true)?.node?.let { + it.elementType == KtTokens.LPAR || it.elementType == KtTokens.AT + } == true) { emit(node.startOffset, "Unexpected space before \"${node.text}\"", true) if (autoCorrect) { prevLeaf.node.treeParent.removeChild(prevLeaf.node) @@ -41,11 +46,10 @@ class SpacingAroundCurlyRule : Rule("curly-spacing") { node.parent.node.elementType == KtNodeTypes.CLASS_BODY)) { emit(node.startOffset, "Unexpected newline before \"${node.text}\"", true) if (autoCorrect) { - (prevLeaf.node as LeafPsiElement).replaceWithText(" ") + (prevLeaf.node as LeafPsiElement).rawReplaceWithText(" ") } } - } else - if (node.textMatches("}")) { + } else if (node.textMatches("}")) { spacingBefore = prevLeaf is PsiWhiteSpace || prevLeaf?.node?.elementType == KtTokens.LBRACE spacingAfter = nextLeaf == null || nextLeaf is PsiWhiteSpace || shouldNotToBeSeparatedBySpace(nextLeaf) if (nextLeaf is PsiWhiteSpace && !nextLeaf.textContains('\n') && diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt index 15bb5597..ec31bf9b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt @@ -29,8 +29,11 @@ class SpacingAroundKeywordRule : Rule("keyword-spacing") { private val keywordsWithoutSpaces = TokenSet.create(KtTokens.GET_KEYWORD, KtTokens.SET_KEYWORD) - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement) { if (tokenSet.contains(node.elementType) && node.nextLeaf() !is PsiWhiteSpace) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt index 4bc9448a..06dd72ee 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt @@ -36,24 +36,36 @@ import org.jetbrains.kotlin.psi.KtSuperExpression import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.KtTypeParameterList import org.jetbrains.kotlin.psi.KtValueArgument +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class SpacingAroundOperatorsRule : Rule("op-spacing") { private val tokenSet = TokenSet.create(MUL, PLUS, MINUS, DIV, PERC, LT, GT, LTEQ, GTEQ, EQEQEQ, EXCLEQEQEQ, EQEQ, EXCLEQ, ANDAND, OROR, ELVIS, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ, ARROW) - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (tokenSet.contains(node.elementType) && node is LeafPsiElement && !node.isPartOf(KtPrefixExpression::class) && // not unary - !node.isPartOf(KtTypeParameterList::class) && // fun <T>fn(): T {} !node.isPartOf(KtTypeArgumentList::class) && // C<T> - !node.isPartOf(KtValueArgument::class) && // fn(*array) + !(node.elementType == MUL && node.isPartOf(KtValueArgument::class)) && // fn(*array) !node.isPartOf(KtImportDirective::class) && // import * !node.isPartOf(KtSuperExpression::class) // super<T> ) { - val spacingBefore = PsiTreeUtil.prevLeaf(node, true) is PsiWhiteSpace - val spacingAfter = PsiTreeUtil.nextLeaf(node, true) is PsiWhiteSpace + if ((node.elementType == GT || node.elementType == LT) && + // fun <T>fn(): T {} + node.getNonStrictParentOfType(KtTypeParameterList::class.java)?.parent?.node?.elementType != + KtStubElementTypes.FUNCTION) { + return + } + val spacingBefore = PsiTreeUtil.prevLeaf(node, true) is PsiWhiteSpace || + node.elementType == GT + val spacingAfter = PsiTreeUtil.nextLeaf(node, true) is PsiWhiteSpace || + node.elementType == LT when { !spacingBefore && !spacingAfter -> { emit(node.startOffset, "Missing spacing around \"${node.text}\"", true) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt new file mode 100644 index 00000000..d7bedbea --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt @@ -0,0 +1,42 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens + +class SpacingAroundRangeOperatorRule : Rule("range-spacing") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtTokens.RANGE) { + val prevLeaf = PsiTreeUtil.prevLeaf(node.psi, true) + val nextLeaf = PsiTreeUtil.nextLeaf(node.psi, true) + when { + prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> { + emit(node.startOffset, "Unexpected spacing around \"..\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + } + prevLeaf is PsiWhiteSpace -> { + emit(prevLeaf.node.startOffset, "Unexpected spacing before \"..\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + } + nextLeaf is PsiWhiteSpace -> { + emit(nextLeaf.node.startOffset, "Unexpected spacing after \"..\"", true) + if (autoCorrect) { + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 9fd9190c..87f06b42 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -6,6 +6,9 @@ import com.github.shyiko.ktlint.core.RuleSetProvider class StandardRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet("standard", + ChainWrappingRule(), + CommentSpacingRule(), + FilenameRule(), FinalNewlineRule(), // disabled until it's clear how to reconcile difference in Intellij & Android Studio import layout // ImportOrderingRule(), @@ -17,17 +20,21 @@ class StandardRuleSetProvider : RuleSetProvider { NoEmptyClassBodyRule(), // disabled until it's clear what to do in case of `import _.it` // NoItParamInMultilineLambdaRule(), + NoLineBreakAfterElseRule(), + NoLineBreakBeforeAssignmentRule(), NoMultipleSpacesRule(), NoSemicolonsRule(), NoTrailingSpacesRule(), NoUnitReturnRule(), NoUnusedImportsRule(), NoWildcardImportsRule(), + ParameterListWrappingRule(), SpacingAroundColonRule(), SpacingAroundCommaRule(), SpacingAroundCurlyRule(), SpacingAroundKeywordRule(), SpacingAroundOperatorsRule(), + SpacingAroundRangeOperatorRule(), StringTemplateRule() ) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt index f58a4bfe..e63d7927 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt @@ -32,26 +32,27 @@ class StringTemplateRule : Rule("string-template") { if (dotQualifiedExpression?.node?.elementType == KtStubElementTypes.DOT_QUALIFIED_EXPRESSION) { val callExpression = dotQualifiedExpression!!.lastChild val dot = callExpression.prevSibling - if (dot.node.elementType == KtTokens.DOT && callExpression.text == "toString()" && - dotQualifiedExpression.firstChild.node.elementType != KtNodeTypes.SUPER_EXPRESSION) { - emit(dot.node.startOffset, "Redundant 'toString()' call in string template", true) + if (dot?.node?.elementType == KtTokens.DOT && + callExpression.text == "toString()" && + dotQualifiedExpression.firstChild?.node?.elementType != KtNodeTypes.SUPER_EXPRESSION) { + emit(dot.node.startOffset, "Redundant \"toString()\" call in string template", true) if (autoCorrect) { node.removeChild(dot.node) node.removeChild(callExpression.node) } } } - } - if (elementType == KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY && - node.text.let { it.substring(2, it.length - 1) }.all { it.isPartOfIdentifier() } && - (node.treeNext.elementType == KtTokens.CLOSING_QUOTE || - (node.psi.nextSibling.node.elementType == KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY && - !node.psi.nextSibling.text[0].isPartOfIdentifier()))) { - emit(node.treePrev.startOffset + 2, "Redundant curly braces", true) - if (autoCorrect) { - // fixme: a proper way would be to downcast to SHORT_STRING_TEMPLATE_ENTRY - (node.psi.firstChild as LeafPsiElement).rawReplaceWithText("$") // entry start - (node.psi.lastChild as LeafPsiElement).rawReplaceWithText("") // entry end + if (node.text.startsWith("${'$'}{") && + node.text.let { it.substring(2, it.length - 1) }.all { it.isPartOfIdentifier() } && + (node.treeNext.elementType == KtTokens.CLOSING_QUOTE || + (node.psi.nextSibling.node.elementType == KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY && + !node.psi.nextSibling.text[0].isPartOfIdentifier()))) { + emit(node.treePrev.startOffset + 2, "Redundant curly braces", true) + if (autoCorrect) { + // fixme: a proper way would be to downcast to SHORT_STRING_TEMPLATE_ENTRY + (node.psi.firstChild as LeafPsiElement).rawReplaceWithText("$") // entry start + (node.psi.lastChild as LeafPsiElement).rawReplaceWithText("") // entry end + } } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt index 2acc5c2d..d9e03b1e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt @@ -1,12 +1,20 @@ package com.github.shyiko.ktlint.ruleset.standard +import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.psi.KtStringTemplateEntry import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType import kotlin.reflect.KClass internal fun PsiElement.isPartOf(clazz: KClass<out PsiElement>) = getNonStrictParentOfType(clazz.java) != null internal fun PsiElement.isPartOfString() = isPartOf(KtStringTemplateEntry::class) +internal fun PsiElement.prevLeaf(): PsiElement? = PsiTreeUtil.prevLeaf(this) +internal fun PsiElement.nextLeaf(): PsiElement? = PsiTreeUtil.nextLeaf(this) +internal fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { + cb(this) + this.getChildren(null).forEach { it.visit(cb) } +} -internal fun <T>List<T>.head() = this.subList(0, this.size - 1) -internal fun <T>List<T>.tail() = this.subList(1, this.size) +internal fun <T> List<T>.head() = this.subList(0, this.size - 1) +internal fun <T> List<T>.tail() = this.subList(1, this.size) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRuleTest.kt new file mode 100644 index 00000000..b96cf10b --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRuleTest.kt @@ -0,0 +1,14 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import org.testng.annotations.Test + +class ChainWrappingRuleTest { + + @Test + fun testLint() = + testLintUsingResource(ChainWrappingRule()) + + @Test + fun testFormat() = + testFormatUsingResource(ChainWrappingRule()) +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRuleTest.kt new file mode 100644 index 00000000..1b8894f7 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRuleTest.kt @@ -0,0 +1,64 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.format +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test + +class CommentSpacingRuleTest { + + @Test + fun testLintValidCommentSpacing() { + assertThat(CommentSpacingRule().lint( + """ + // + // comment + var debugging = false // comment + var debugging = false // comment//word + // comment + """.trimIndent() + )).isEmpty() + } + + @Test + fun testLintInvalidCommentSpacing() { + assertThat(CommentSpacingRule().lint( + """ + //comment + var debugging = false// comment + var debugging = false //comment + var debugging = false//comment + //comment + """.trimIndent() + )).isEqualTo(listOf( + LintError(1, 1, "comment-spacing", "Missing space after //"), + LintError(2, 22, "comment-spacing", "Missing space before //"), + LintError(3, 23, "comment-spacing", "Missing space after //"), + LintError(4, 22, "comment-spacing", "Missing space before //"), + LintError(4, 22, "comment-spacing", "Missing space after //"), + LintError(5, 5, "comment-spacing", "Missing space after //") + )) + } + + @Test + fun testFormatInvalidCommentSpacing() { + assertThat(CommentSpacingRule().format( + """ + //comment + var debugging = false// comment + var debugging = false //comment + var debugging = false//comment + //comment + """.trimIndent() + )).isEqualTo( + """ + // comment + var debugging = false // comment + var debugging = false // comment + var debugging = false // comment + // comment + """.trimIndent() + ) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt new file mode 100644 index 00000000..84e7ad4a --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt @@ -0,0 +1,129 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test + +class FilenameRuleTest { + + @Test + fun testMatchingSingleClassName() { + for (src in listOf( + "class A", + "data class A(val v: Int)", + "sealed class A", + "interface A", + "object A", + "enum class A {A}", + "typealias A = Set<Network.Node>", + // >1 declaration case + "class B\nfun A.f() {}" + )) { + assertThat(FilenameRule().lint( + """ + /* + * license + */ + @file:JvmName("Foo") + package x + import y.Z + $src + // + """.trimIndent(), + fileName("/some/path/A.kt") + )).isEmpty() + } + } + + @Test + fun testNonMatchingSingleClassName() { + for (src in mapOf( + "class A" to "class", + "data class A(val v: Int)" to "class", + "sealed class A" to "class", + "interface A" to "interface", + "object A" to "object", + "enum class A {A}" to "class", + "typealias A = Set<Network.Node>" to "typealias" + )) { + assertThat(FilenameRule().lint( + """ + /* + * license + */ + @file:JvmName("Foo") + package x + import y.Z + ${src.key} + // + """.trimIndent(), + fileName("/some/path/B.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "${src.value} A should be declared in a file named A.kt") + )) + } + } + + @Test + fun testFileWithoutTopLevelDeclarations() { + assertThat(FilenameRule().lint( + """ + /* + * copyright + */ + """.trimIndent(), + fileName("A.kt") + )).isEmpty() + } + + @Test + fun testMultipleTopLevelClasses() { + assertThat(FilenameRule().lint( + """ + class B + class C + """.trimIndent(), + fileName("A.kt") + )).isEmpty() + } + + @Test + fun testMultipleNonTopLevelClasses() { + assertThat(FilenameRule().lint( + """ + class B { + class C + class D + } + """.trimIndent(), + fileName("A.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "class B should be declared in a file named B.kt") + )) + } + + @Test + fun testCaseSensitiveMatching() { + assertThat(FilenameRule().lint( + """ + interface Woohoo + """.trimIndent(), + fileName("woohoo.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "interface Woohoo should be declared in a file named Woohoo.kt") + )) + } + + @Test + fun testIgnoreKotlinScriptFiles() { + assertThat(FilenameRule().lint( + """ + class B + """.trimIndent(), + fileName("A.kts") + )).isEmpty() + } + + private fun fileName(fileName: String) = mapOf("file_path" to fileName) +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRuleTest.kt index 2f911990..bea860f6 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRuleTest.kt @@ -30,6 +30,11 @@ class FinalNewlineRuleTest { "fun name() {\n}\n", mapOf("insert_final_newline" to "true") )).isEmpty() + assertThat(FinalNewlineRule().lint( + "fun main() {\n}\n\n\n", + mapOf("insert_final_newline" to "true"), + script = true + )).isEmpty() // false assertThat(FinalNewlineRule().lint( "fun name() {\n}", diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRuleTest.kt index 539b45aa..636f812c 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRuleTest.kt @@ -8,7 +8,7 @@ import org.testng.annotations.Test class IndentationRuleTest { @Test - fun testRule() { + fun testLint() { assertThat(IndentationRule().lint( """ /** @@ -23,7 +23,7 @@ class IndentationRuleTest { val b = builder().setX().setY() .build() val c = builder("long_string" + - "") + "") } class A { @@ -33,52 +33,29 @@ class IndentationRuleTest { } """.trimIndent() )).isEqualTo(listOf( - LintError(12, 1, "indent", "Unexpected indentation (3) (it should be multiple of 4)") + LintError(12, 1, "indent", "Unexpected indentation (3) (it should be 4)"), + // fixme: expected indent should not depend on the "previous" line value + LintError(13, 1, "indent", "Unexpected indentation (9) (it should be 7)") )) } @Test - fun testVerticallyAlignedParametersDoNotTriggerAnError() { + fun testLintCustomIndentSize() { assertThat(IndentationRule().lint( """ - data class D(val a: Any, - @Test val b: Any, - val c: Any = 0) { - } - - data class D2( - val a: Any, - val b: Any, - val c: Any - ) { - } - - fun f(val a: Any, - val b: Any, - val c: Any) { - } - - fun f2( - val a: Any, - val b: Any, - val c: Any - ) { + fun main() { + val v = "" + println(v) } - """.trimIndent() - )).isEmpty() - assertThat(IndentationRule().lint( - """ - class A( - // - ) {} - """.trimIndent() + """.trimIndent(), + mapOf("indent_size" to "3") )).isEqualTo(listOf( - LintError(2, 1, "indent", "Unexpected indentation (3) (it should be multiple of 4)") + LintError(3, 1, "indent", "Unexpected indentation (4) (it should be 3)") )) } @Test - fun testWithCustomIndentSize() { + fun testLintCustomIndentSizeValid() { assertThat(IndentationRule().lint( """ /** @@ -100,7 +77,7 @@ class IndentationRuleTest { } @Test - fun testErrorWithCustomIndentSize() { + fun testLintIndentSizeUnset() { assertThat(IndentationRule().lint( """ fun main() { @@ -108,22 +85,102 @@ class IndentationRuleTest { println(v) } """.trimIndent(), - mapOf("indent_size" to "3") + mapOf("indent_size" to "unset") + )).isEmpty() + } + + @Test + fun testLintWithContinuationIndentSizeSet() { + // gcd(indent_size, continuation_indent_size) == 2 + assertThat(IndentationRule().lint( + """ + fun main() { + val v = "" + .call() + call() + } + """.trimIndent(), + mapOf("indent_size" to "4", "continuation_indent_size" to "6") )).isEqualTo(listOf( - LintError(3, 1, "indent", "Unexpected indentation (4) (it should be multiple of 3)") + LintError(4, 1, "indent", "Unexpected indentation (5) (it should be 2)") )) + assertThat(IndentationRule().lint( + """ + fun main() { + val v = "" + .call() + call() + } + """.trimIndent(), + mapOf("indent_size" to "4", "continuation_indent_size" to "2") + )).isEqualTo(listOf( + LintError(4, 1, "indent", "Unexpected indentation (5) (it should be 2)") + )) + // gcd(indent_size, continuation_indent_size) == 1 equals no indent check + assertThat(IndentationRule().lint( + """ + fun main() { + val v = "" + .call() + .call() + .call() + } + """.trimIndent(), + mapOf("indent_size" to "4", "continuation_indent_size" to "3") + )).isEmpty() } + // https://kotlinlang.org/docs/reference/coding-conventions.html#method-call-formatting @Test - fun testErrorWithIndentSizeUnset() { + fun testLintMultilineFunctionCall() { assertThat(IndentationRule().lint( """ fun main() { - val v = "" - println(v) + fn(a, + b, + c) + } + """.trimIndent() + )).isEqualTo(listOf( + LintError(3, 1, "indent", "Unexpected indentation (7) (it should be 8)"), + LintError(4, 1, "indent", "Unexpected indentation (7) (it should be 8)") + )) + } + + @Test + fun testLintCommentsAreIgnored() { + assertThat(IndentationRule().lint( + """ + fun funA(argA: String) = + // comment + // comment + call(argA) + fun main() { + addOnLayoutChangeListener(object : View.OnLayoutChangeListener { + // comment + override fun onLayoutChange( + ) + }) } """.trimIndent(), - mapOf("indent_size" to "unset") + mapOf("indent_size" to "4") + )).isEqualTo(listOf( + LintError(7, 1, "indent", "Unexpected indentation (1) (it should be 8)") + )) + } + + @Test(description = "https://github.com/shyiko/ktlint/issues/180") + fun testLintWhereClause() { + assertThat(IndentationRule().lint( + """ + class BiAdapter<C : RecyclerView.ViewHolder, V1 : C, V2 : C, out A1, out A2>( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter<C>() + where A1 : RecyclerView.Adapter<V1>, A1 : ComposableAdapter.ViewTypeProvider, + A2 : RecyclerView.Adapter<V2>, A2 : ComposableAdapter.ViewTypeProvider { + } + """.trimIndent() )).isEmpty() } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRuleTest.kt index 723b4d3f..52659f8a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRuleTest.kt @@ -1,5 +1,8 @@ package com.github.shyiko.ktlint.ruleset.standard +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat import org.testng.annotations.Test class MaxLineLengthRuleTest { @@ -10,7 +13,37 @@ class MaxLineLengthRuleTest { } @Test + fun testErrorSupression() { + assertThat(MaxLineLengthRule().lint( + """ + fun main(vaaaaaaaaaaaaaaaaaaaaaaar: String) { // ktlint-disable max-line-length + println("teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeext") + /* ktlint-disable max-line-length */ + println("teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeext") + } + """.trimIndent(), + userData = mapOf("max_line_length" to "40") + )).isEqualTo(listOf( + LintError(2, 1, "max-line-length", "Exceeded max line length (40)") + )) + } + + @Test fun testLintOff() { testLintUsingResource(MaxLineLengthRule(), userData = mapOf("max_line_length" to "off"), qualifier = "off") } + + @Test + fun testRangeSearch() { + for (i in 0 until 10) { + assertThat(RangeTree((0..i).asSequence().toList()).query(Int.MIN_VALUE, Int.MAX_VALUE).toString()) + .isEqualTo((0..i).asSequence().toList().toString()) + } + assertThat(RangeTree(emptyList()).query(1, 5).toString()).isEqualTo("[]") + assertThat(RangeTree((5 until 10).asSequence().toList()).query(1, 5).toString()).isEqualTo("[]") + assertThat(RangeTree((5 until 10).asSequence().toList()).query(3, 7).toString()).isEqualTo("[5, 6]") + assertThat(RangeTree((5 until 10).asSequence().toList()).query(7, 12).toString()).isEqualTo("[7, 8, 9]") + assertThat(RangeTree((5 until 10).asSequence().toList()).query(10, 15).toString()).isEqualTo("[]") + assertThat(RangeTree(listOf(1, 5, 10)).query(3, 4).toString()).isEqualTo("[]") + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRuleTest.kt index 3c2d1fb4..24372bbc 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRuleTest.kt @@ -13,7 +13,7 @@ class ModifierOrderRuleTest { // pretty much every line below should trip an error assertThat(ModifierOrderRule().lint( """ - abstract open class A { // open is here for test purposes only, otherwise it's redundant + abstract @Deprecated open class A { // open is here for test purposes only, otherwise it's redundant open protected val v = "" open suspend internal fun f(v: Any): Any = "" lateinit public var lv: String @@ -22,9 +22,19 @@ class ModifierOrderRuleTest { class B : A() { override public val v = "" - override suspend fun f(v: Any): Any = "" - override tailrec fun findFixPoint(x: Double): Double + suspend override fun f(v: Any): Any = "" + tailrec override fun findFixPoint(x: Double): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) + override @Annotation fun getSomething() = "" + override @Annotation suspend public @Woohoo(data = "woohoo") fun doSomething() = "" + @A + @B(v = [ + "foo", + "baz", + "bar" + ]) + @C + suspend public fun returnsSomething() = "" companion object { const internal val V = "" @@ -32,15 +42,18 @@ class ModifierOrderRuleTest { } """.trimIndent() )).isEqualTo(listOf( - LintError(1, 1, "modifier-order", "Incorrect modifier order (should be \"open abstract\")"), + LintError(1, 1, "modifier-order", "Incorrect modifier order (should be \"@Annotation... open abstract\")"), LintError(2, 5, "modifier-order", "Incorrect modifier order (should be \"protected open\")"), LintError(3, 5, "modifier-order", "Incorrect modifier order (should be \"internal open suspend\")"), LintError(4, 5, "modifier-order", "Incorrect modifier order (should be \"public lateinit\")"), LintError(5, 5, "modifier-order", "Incorrect modifier order (should be \"abstract tailrec\")"), LintError(9, 5, "modifier-order", "Incorrect modifier order (should be \"public override\")"), - LintError(10, 5, "modifier-order", "Incorrect modifier order (should be \"suspend override\")"), - LintError(11, 5, "modifier-order", "Incorrect modifier order (should be \"tailrec override\")"), - LintError(15, 8, "modifier-order", "Incorrect modifier order (should be \"internal const\")") + LintError(10, 5, "modifier-order", "Incorrect modifier order (should be \"override suspend\")"), + LintError(11, 5, "modifier-order", "Incorrect modifier order (should be \"override tailrec\")"), + LintError(13, 5, "modifier-order", "Incorrect modifier order (should be \"@Annotation... override\")"), + LintError(14, 5, "modifier-order", "Incorrect modifier order (should be \"@Annotation... public override suspend\")"), + LintError(15, 5, "modifier-order", "Incorrect modifier order (should be \"@Annotation... public suspend\")"), + LintError(25, 8, "modifier-order", "Incorrect modifier order (should be \"internal const\")") )) } @@ -48,7 +61,7 @@ class ModifierOrderRuleTest { fun testFormat() { assertThat(ModifierOrderRule().format( """ - abstract open class A { // open is here for test purposes only, otherwise it's redundant + abstract @Deprecated open class A { // open is here for test purposes only, otherwise it's redundant open protected val v = "" open suspend internal fun f(v: Any): Any = "" lateinit public var lv: String @@ -57,9 +70,19 @@ class ModifierOrderRuleTest { class B : A() { override public val v = "" - override suspend fun f(v: Any): Any = "" - override tailrec fun findFixPoint(x: Double): Double + suspend override fun f(v: Any): Any = "" + tailrec override fun findFixPoint(x: Double): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) + override @Annotation fun getSomething() = "" + suspend @Annotation override public @Woohoo(data = "woohoo") fun doSomething() = "" + @A + @B(v = [ + "foo", + "baz", + "bar" + ]) + @C + suspend public fun returnsSomething() = "" companion object { const internal val V = "" @@ -68,7 +91,7 @@ class ModifierOrderRuleTest { """ )).isEqualTo( """ - open abstract class A { // open is here for test purposes only, otherwise it's redundant + @Deprecated open abstract class A { // open is here for test purposes only, otherwise it's redundant protected open val v = "" internal open suspend fun f(v: Any): Any = "" public lateinit var lv: String @@ -77,9 +100,19 @@ class ModifierOrderRuleTest { class B : A() { public override val v = "" - suspend override fun f(v: Any): Any = "" - tailrec override fun findFixPoint(x: Double): Double + override suspend fun f(v: Any): Any = "" + override tailrec fun findFixPoint(x: Double): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) + @Annotation override fun getSomething() = "" + @Annotation @Woohoo(data = "woohoo") public override suspend fun doSomething() = "" + @A + @B(v = [ + "foo", + "baz", + "bar" + ]) + @C + public suspend fun returnsSomething() = "" companion object { internal const val V = "" diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRuleTest.kt index 60da450b..5054d992 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRuleTest.kt @@ -38,6 +38,19 @@ class NoConsecutiveBlankLinesRuleTest { } @Test + fun testLintAtTheEndOfFile() { + assertThat(NoConsecutiveBlankLinesRule().lint( + """ + fun main() { + } + + + """.trimIndent() + )).isEqualTo(listOf( + LintError(4, 1, "no-consecutive-blank-lines", "Needless blank line(s)"))) + } + + @Test fun testLintInString() { assertThat(NoConsecutiveBlankLinesRule().lint( "fun main() {println(\"\"\"\n\n\n\"\"\")}")).isEmpty() @@ -94,4 +107,23 @@ class NoConsecutiveBlankLinesRuleTest { """ ) } + + @Test + fun testFormatAtTheEndOfFile() { + assertThat(NoConsecutiveBlankLinesRule().format( + """ + fun main() { + } + + + """, + script = true + )).isEqualTo( + """ + fun main() { + } + + """ + ) + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRuleTest.kt index e115bd03..3bcd180f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRuleTest.kt @@ -1,5 +1,7 @@ package com.github.shyiko.ktlint.ruleset.standard +import com.github.shyiko.ktlint.test.format +import org.assertj.core.api.Assertions.assertThat import org.testng.annotations.Test class NoEmptyClassBodyRuleTest { @@ -13,4 +15,10 @@ class NoEmptyClassBodyRuleTest { fun testFormat() { testFormatUsingResource(NoEmptyClassBodyRule()) } + + @Test + fun testFormatEmptyClassBodyAtTheEndOfFile() { + assertThat(NoEmptyClassBodyRule().format("class A {}\n")).isEqualTo("class A\n") + assertThat(NoEmptyClassBodyRule().format("class A {}")).isEqualTo("class A") + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRuleTest.kt new file mode 100644 index 00000000..8b645ecf --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRuleTest.kt @@ -0,0 +1,127 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.format +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions +import org.testng.annotations.Test + +class NoLineBreakAfterElseRuleTest { + + @Test + fun testViolationForLineBreakBetweenElseAndIf() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else + if (conditionB()) { + doAnotherThing() + } + } + """.trimIndent() + )).isEqualTo(listOf( + LintError(5, 1, "no-line-break-after-else", "Unexpected line break after \"else\"") + )) + } + + @Test + fun testFixViolationForLineBreakBetweenElseAndIf() { + Assertions.assertThat(NoLineBreakAfterElseRule().format( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else + if (conditionB()) { + doAnotherThing() + } + } + """.trimIndent() + )).isEqualTo( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else if (conditionB()) { + doAnotherThing() + } + } + """.trimIndent() + ) + } + + @Test + fun testValidElseIf() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else if (conditionB()) { + doAnotherThing() + } + } + """.trimIndent() + )).isEmpty() + } + + @Test + fun testValidSimpleElse() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else { + doAnotherThing() + } + } + """.trimIndent() + )).isEmpty() + } + + @Test + fun testViolationForLineBreakBetweenElseAndBracket() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) { + doSomething() + } else + { + doAnotherThing() + } + } + """.trimIndent() + )).isEqualTo(listOf( + LintError(5, 1, "no-line-break-after-else", "Unexpected line break after \"else\"") + )) + } + + @Test + fun testViolationWhenBracketOmitted() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) + doSomething() + else + doAnotherThing() + } + """.trimIndent() + )).isEmpty() + } + + @Test + fun testValidWhenBracketOmitted() { + Assertions.assertThat(NoLineBreakAfterElseRule().lint( + """ + fun funA() { + if (conditionA()) doSomething() else doAnotherThing() + } + """.trimIndent() + )).isEmpty() + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRuleTest.kt new file mode 100644 index 00000000..55ac3e39 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRuleTest.kt @@ -0,0 +1,69 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.format +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test + +const val ruleId = "no-line-break-before-assignment" + +class NoLineBreakBeforeAssignmentRuleTest { + @Test + fun testAllPartsOnSameLineIsValid() { + assertThat(NoLineBreakBeforeAssignmentRule().lint( + """ + val valA = "" + """.trimIndent() + )).isEmpty() + } + + @Test + fun testLineBreakAfterAssignmentIsValid() { + assertThat(NoLineBreakBeforeAssignmentRule().lint( + """ + val valA = + "" + """.trimIndent() + )).isEmpty() + } + + @Test + fun testLineBreakBeforeAssignmentIsViolation() { + assertThat(NoLineBreakBeforeAssignmentRule().lint( + """ + val valA + = "" + """.trimIndent() + )).isEqualTo(listOf( + LintError(2, 7, ruleId, "Line break before assignment is not allowed") + )) + } + + @Test + fun testViolationInFunction() { + assertThat(NoLineBreakBeforeAssignmentRule().lint( + """ + fun funA() + = "" + """.trimIndent() + )).isEqualTo(listOf( + LintError(2, 7, ruleId, "Line break before assignment is not allowed") + )) + } + + @Test + fun testFixViolationByRemovingLineBreakFromLeftAndPutItOnRightSide() { + assertThat(NoLineBreakBeforeAssignmentRule().format( + """ + fun funA() + = "" + """.trimIndent() + )).isEqualTo( + """ + fun funA() = + "" + """.trimIndent() + ) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt index 2384ffa9..ad5f6cc9 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt @@ -48,6 +48,50 @@ class NoUnusedImportsRuleTest { } @Test + fun testLintIssue204() { + assertThat(NoUnusedImportsRule().lint( + """ + package com.example.another + + import com.example.anotherThing + + class Foo { + val bar = anotherThing + } + """.trimIndent() + )).isEmpty() + } + + @Test + fun testLintDestructuringAssignment() { + assertThat(NoUnusedImportsRule().lint( + """ + import p.component6 + + fun main() { + val (one, two, three, four, five, six) = someList + } + """.trimIndent() + )).isEmpty() + assertThat(NoUnusedImportsRule().lint( + """ + import p.component6 + import p.component2 + import p.component100 + import p.component + import p.component12woohoo + + fun main() { + val (one, two, three, four, five, six) = someList + } + """.trimIndent() + )).isEqualTo(listOf( + LintError(4, 1, "no-unused-imports", "Unused import"), + LintError(5, 1, "no-unused-imports", "Unused import") + )) + } + + @Test fun testLintKDocLinkImport() { assertThat(NoUnusedImportsRule().lint( """ diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt new file mode 100644 index 00000000..24c048e9 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRuleTest.kt @@ -0,0 +1,498 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.format +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test + +class ParameterListWrappingRuleTest { + + @Test + fun testLintClassParameterList() { + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA(paramA: String, paramB: String, + paramC: String) + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 14, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 30, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(2, 14, "parameter-list-wrapping", "Unexpected indentation (expected 4, actual 13)"), + LintError(2, 28, "parameter-list-wrapping", """Missing newline before ")"""") + ) + ) + } + + @Test + fun testLintClassParameterListWhenMaxLineLengthExceeded() { + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA(paramA: String, paramB: String, paramC: String) + """.trimIndent(), + userData = mapOf("max_line_length" to "10") + ) + ).isEqualTo( + listOf( + LintError(1, 14, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 30, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 46, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 60, "parameter-list-wrapping", """Missing newline before ")"""") + ) + ) + // corner case + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA(paramA: String) + class ClassA(paramA: String) + class ClassA(paramA: String) + """.trimIndent(), + userData = mapOf("max_line_length" to "28") + ) + ).isEqualTo( + listOf( + LintError(2, 15, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(2, 29, "parameter-list-wrapping", "Missing newline before \")\"") + ) + ) + } + + @Test + fun testLintClassParameterListValid() { + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA(paramA: String, paramB: String, paramC: String) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun testLintClassParameterListValidMultiLine() { + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun testFormatClassParameterList() { + assertThat( + ParameterListWrappingRule().format( + """ + class ClassA(paramA: String, paramB: String, + paramC: String) + """.trimIndent() + ) + ).isEqualTo( + """ + class ClassA( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + } + + @Test + fun testFormatClassParameterListWhenMaxLineLengthExceeded() { + assertThat( + ParameterListWrappingRule().format( + """ + class ClassA(paramA: String, paramB: String, paramC: String) + """.trimIndent(), + userData = mapOf("max_line_length" to "10") + ) + ).isEqualTo( + """ + class ClassA( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + } + + @Test + fun testLintFunctionParameterList() { + assertThat( + ParameterListWrappingRule().lint( + """ + fun f(a: Any, + b: Any, + c: Any) { + } + """.trimIndent() + )).isEqualTo( + listOf( + LintError(1, 7, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(2, 7, "parameter-list-wrapping", "Unexpected indentation (expected 4, actual 6)"), + LintError(3, 7, "parameter-list-wrapping", "Unexpected indentation (expected 4, actual 6)"), + LintError(3, 13, "parameter-list-wrapping", """Missing newline before ")"""") + ) + ) + } + + @Test + fun testLintFunctionParameterListWhenMaxLineLengthExceeded() { + assertThat( + ParameterListWrappingRule().lint( + """ + fun f(a: Any, b: Any, c: Any) { + } + """.trimIndent(), + userData = mapOf("max_line_length" to "10") + )).isEqualTo( + listOf( + LintError(1, 7, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 15, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 23, "parameter-list-wrapping", "Parameter should be on a separate line (unless all parameters can fit a single line)"), + LintError(1, 29, "parameter-list-wrapping", """Missing newline before ")"""") + ) + ) + } + + @Test + fun testFormatFunctionParameterList() { + assertThat( + ParameterListWrappingRule().format( + """ + fun f(a: Any, + b: Any, + c: Any) { + } + """.trimIndent() + )).isEqualTo( + """ + fun f( + a: Any, + b: Any, + c: Any + ) { + } + """.trimIndent() + ) + } + + @Test + fun testFormatFunctionParameterListWhenMaxLineLengthExceeded() { + assertThat( + ParameterListWrappingRule().format( + """ + fun f(a: Any, b: Any, c: Any) { + } + """.trimIndent(), + userData = mapOf("max_line_length" to "10") + ) + ).isEqualTo( + """ + fun f( + a: Any, + b: Any, + c: Any + ) { + } + """.trimIndent() + ) + } + + @Test + fun testLambdaParametersAreIgnored() { + assertThat( + ParameterListWrappingRule().lint( + """ + val fieldExample = + LongNameClass { paramA, + paramB, + paramC -> + ClassB(paramA, paramB, paramC) + } + """.trimIndent() + )).isEmpty() + } + + @Test + fun testFormatPreservesIndent() { + assertThat( + ParameterListWrappingRule().format( + """ + class A { + fun f(a: Any, + b: Any, + c: Any) { + } + } + """.trimIndent() + )).isEqualTo( + """ + class A { + fun f( + a: Any, + b: Any, + c: Any + ) { + } + } + """.trimIndent() + ) + } + + @Test + fun testFormatPreservesIndentWithAnnotations() { + assertThat( + ParameterListWrappingRule().format( + """ + class A { + fun f(@Annotation + a: Any, + @Annotation([ + "v1", + "v2" + ]) + b: Any, + c: Any = + false, + @Annotation d: Any) { + } + } + """.trimIndent() + )).isEqualTo( + """ + class A { + fun f( + @Annotation + a: Any, + @Annotation([ + "v1", + "v2" + ]) + b: Any, + c: Any = + false, + @Annotation d: Any + ) { + } + } + """.trimIndent() + ) + } + + @Test + fun testFormatCorrectsRPARIndentIfNeeded() { + assertThat( + ParameterListWrappingRule().format( + """ + class A { + fun f(a: Any, + b: Any, + c: Any + ) { + } + } + """.trimIndent() + )).isEqualTo( + """ + class A { + fun f( + a: Any, + b: Any, + c: Any + ) { + } + } + """.trimIndent() + ) + } + + @Test + fun testFormatNestedDeclarations() { + assertThat( + ParameterListWrappingRule().format( + """ + fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, + canBeAutoCorrected: Boolean) -> Unit + ) {} + """.trimIndent() + )).isEqualTo( + """ + fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: ( + offset: Int, + errorMessage: String, + canBeAutoCorrected: Boolean + ) -> Unit + ) {} + """.trimIndent() + ) + } + + @Test + fun testFormatNestedDeclarationsWhenMaxLineLengthExceeded() { + assertThat( + ParameterListWrappingRule().format( + """ + fun visit(node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {} + """.trimIndent(), + userData = mapOf("max_line_length" to "10") + )).isEqualTo( + """ + fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: ( + offset: Int, + errorMessage: String, + canBeAutoCorrected: Boolean + ) -> Unit + ) {} + """.trimIndent() + ) + } + + @Test + fun testFormatNestedDeclarationsValid() { + assertThat( + ParameterListWrappingRule().format( + """ + fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) {} + """.trimIndent() + )).isEqualTo( + """ + fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) {} + """.trimIndent() + ) + } + + @Test + fun testCommentsAreIgnored() { + assertThat(ParameterListWrappingRule().lint( + """ + data class A( + /* + * comment + */ + // + var v: String + ) + """.trimIndent() + )).isEqualTo(listOf( + LintError(6, 4, "parameter-list-wrapping", "Unexpected indentation (expected 4, actual 3)") + )) + } + + @Test + fun testLintClassDanglingLeftParen() { + assertThat( + ParameterListWrappingRule().lint( + """ + class ClassA + ( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(2, 1, "parameter-list-wrapping", """Unnecessary newline before "("""") + ) + ) + } + + @Test + fun testLintFunctionDanglingLeftParen() { + assertThat( + ParameterListWrappingRule().lint( + """ + fun doSomething + ( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(2, 1, "parameter-list-wrapping", """Unnecessary newline before "("""") + ) + ) + } + + @Test + fun testFormatClassDanglingLeftParen() { + assertThat( + ParameterListWrappingRule().format( + """ + class ClassA constructor + ( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + ).isEqualTo( + """ + class ClassA constructor( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + } + + @Test + fun testFormatFunctionDanglingLeftParen() { + assertThat( + ParameterListWrappingRule().format( + """ + fun doSomething + ( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + ).isEqualTo( + """ + fun doSomething( + paramA: String, + paramB: String, + paramC: String + ) + """.trimIndent() + ) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRuleTest.kt index cc71781f..e0bcb9cc 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRuleTest.kt @@ -23,12 +23,38 @@ class SpacingAroundCommaRuleTest { )).isEqualTo(listOf( LintError(2, 10, "comma-spacing", "Missing spacing after \",\"") )) + assertThat(SpacingAroundCommaRule().lint( + """ + some.method(1 , 2) + """.trimIndent(), + script = true + )).isEqualTo(listOf( + LintError(1, 14, "comma-spacing", "Unexpected spacing before \",\"") + )) } @Test fun testFormat() { assertThat(SpacingAroundCommaRule().format("fun main() { x(1,3); x(1, 3) }")) .isEqualTo("fun main() { x(1, 3); x(1, 3) }") + assertThat(SpacingAroundCommaRule().format( + """ + fun fn( + arg1: Int , + arg2: Int + , + + arg3: Int + ) = Unit + """.trimIndent() + )).isEqualTo( + """ + fun fn( + arg1: Int, + arg2: Int, + + arg3: Int + ) = Unit + """.trimIndent()) } } - diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRuleTest.kt index f5d2ad2e..4d50c68a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRuleTest.kt @@ -11,6 +11,7 @@ class SpacingAroundCurlyRuleTest { @Test fun testLint() { assertThat(SpacingAroundCurlyRule().lint("fun emit() { }")).isEmpty() + assertThat(SpacingAroundCurlyRule().lint("fun emit() { val a = a@{ } }")).isEmpty() assertThat(SpacingAroundCurlyRule().lint("fun emit() {}")).isEmpty() assertThat(SpacingAroundCurlyRule().lint("fun main() { val v = if (true){return 0} }")) .isEqualTo(listOf( @@ -88,6 +89,7 @@ class SpacingAroundCurlyRuleTest { val f = { true } } + class A { private val shouldEjectBlock = block@ { (pathProgress ?: return@block false) >= 0.85 } } """.trimIndent() )).isEqualTo( """ @@ -129,6 +131,7 @@ class SpacingAroundCurlyRuleTest { val f = { true } } + class A { private val shouldEjectBlock = block@{ (pathProgress ?: return@block false) >= 0.85 } } """.trimIndent() ) } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt index b8d25bd7..dad897ea 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRuleTest.kt @@ -141,4 +141,3 @@ class SpacingAroundKeywordRuleTest { )) } } - diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRuleTest.kt index a64b4e3c..4bb35915 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRuleTest.kt @@ -1,73 +1,16 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.LintError -import com.github.shyiko.ktlint.test.format -import com.github.shyiko.ktlint.test.lint -import org.assertj.core.api.Assertions.assertThat import org.testng.annotations.Test class SpacingAroundOperatorsRuleTest { @Test fun testLint() { - assertThat(SpacingAroundOperatorsRule().lint( - """ - import a.b.* - fun main() { - val v = 0 - 1 * 2 - val v1 = 0-1*2 - val v2 = -0 - 1 - val v3 = v * 2 - i++ - val y = +1 - var x = 1 in 3..4 - val b = 1 < 2 - fun(a = true) - val res = ArrayList<LintError>() - fn(*arrayOfNulls<Any>(0 * 1)) - fun <T>List<T>.head() {} - val a= "" - d *= 1 - call(*v) - open class A<T> { - open fun x() {} - } - class B<T> : A<T>() { - override fun x() = super<A>.x() - } - } - """.trimIndent() - )).isEqualTo(listOf( - LintError(4, 15, "op-spacing", "Missing spacing around \"-\""), - LintError(4, 17, "op-spacing", "Missing spacing around \"*\""), - LintError(15, 10, "op-spacing", "Missing spacing before \"=\"") - )) + testLintUsingResource(SpacingAroundOperatorsRule()) } @Test fun testFormat() { - assertThat(SpacingAroundOperatorsRule().format( - """ - fun main() { - val v1 = 0-1*2 - val v2 = -0-1 - val v3 = v*2 - i++ - val y = +1 - var x = 1 in 3..4 - } - """.trimIndent() - )).isEqualTo( - """ - fun main() { - val v1 = 0 - 1 * 2 - val v2 = -0 - 1 - val v3 = v * 2 - i++ - val y = +1 - var x = 1 in 3..4 - } - """.trimIndent() - ) + testFormatUsingResource(SpacingAroundOperatorsRule()) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRuleTest.kt new file mode 100644 index 00000000..e836e327 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRuleTest.kt @@ -0,0 +1,16 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import org.testng.annotations.Test + +class SpacingAroundRangeOperatorRuleTest { + + @Test + fun testLint() { + testLintUsingResource(SpacingAroundRangeOperatorRule()) + } + + @Test + fun testFormat() { + testFormatUsingResource(SpacingAroundRangeOperatorRule()) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/package-test.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/package-test.kt index 016cbd46..3c42d4b4 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/package-test.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/package-test.kt @@ -20,14 +20,17 @@ fun testLintUsingResource(rule: Rule, qualifier: String = "", userData: Map<Stri } val input = resourceText.substring(0, dividerIndex) val errors = resourceText.substring(dividerIndex + 1).split('\n').mapNotNull { line -> - if (line.isBlank() || line == "// expect") null else + if (line.isBlank() || line == "// expect") { + null + } else { line.trimMargin("// ").split(':', limit = 3).let { expectation -> if (expectation.size != 3) { throw RuntimeException("$resource expectation must be a triple <line>:<column>:<message>") - // " (<message> is not allowed to contain \":\")") + // " (<message> is not allowed to contain \":\")") } LintError(expectation[0].toInt(), expectation[1].toInt(), rule.id, expectation[2]) } + } } assertThat(rule.lint(input, userData)).isEqualTo(errors) } diff --git a/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format-expected.kt.spec new file mode 100644 index 00000000..481c7b2a --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format-expected.kt.spec @@ -0,0 +1,66 @@ +fun main() { + val anchor = owner.firstChild!! + .siblings(forward = true) + .dropWhile { it is PsiComment || it is PsiWhiteSpace } + val s = foo() + ?: bar + val s = foo() + ?.bar + val s = 1 + + 2 + val s = true && + false + val s = b.equals(o.b) && + g == o.g + val d = 1 + + -1 + val d = 1 + + -1 + when (foo){ + 0 -> { + } + 1 -> { + } + -2 -> { + } + } + if ( + -3 == a() + ) {} + if ( + // comment + -3 == a() + ) {} + if ( + /* comment */ + -3 == a() + ) {} + if (c) + -7 + else + -8 + try { + fn() + } catch(e: Exception) { + -9 + } + var x = + -2 > + (2 + 2) + -3 + // https://github.com/shyiko/ktlint/pull/193 + var x = false && // comment + false + x = false && + /* comment */ + // comment + false + var y = false // comment + .call() + y = false + // comment + .call() + y = false // comment + /* comment */ + .call() +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format.kt.spec new file mode 100644 index 00000000..82af6da4 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/format.kt.spec @@ -0,0 +1,66 @@ +fun main() { + val anchor = owner.firstChild!!. + siblings(forward = true). + dropWhile { it is PsiComment || it is PsiWhiteSpace } + val s = foo() ?: + bar + val s = foo()?. + bar + val s = 1 + + 2 + val s = true + && false + val s = b.equals(o.b) + && g == o.g + val d = 1 + + -1 + val d = 1 + + -1 + when (foo){ + 0 -> { + } + 1 -> { + } + -2 -> { + } + } + if ( + -3 == a() + ) {} + if ( + // comment + -3 == a() + ) {} + if ( + /* comment */ + -3 == a() + ) {} + if (c) + -7 + else + -8 + try { + fn() + } catch(e: Exception) { + -9 + } + var x = + -2 > + (2 + 2) + -3 + // https://github.com/shyiko/ktlint/pull/193 + var x = false // comment + && false + x = false + /* comment */ + // comment + && false + var y = false. // comment + call() + y = false. + // comment + call() + y = false. // comment + /* comment */ + call() +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec new file mode 100644 index 00000000..2770cdef --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec @@ -0,0 +1,46 @@ +fun main() { + val anchor = owner.firstChild!!. + siblings(forward = true). + dropWhile { it is PsiComment || it is PsiWhiteSpace } + val s = foo() ?: + bar + val s = foo()?. + bar + val d = 1 + + 1 + val s = true + && false + val s = b.equals(o.b) + && g == o.g + val s = ((1 + 2) + / 3) + val d = 1 + + -1 + val d = 1 + + -1 + val d = (1 + + 1) + fn(1, + -1) + fn( + *typedArray<EventListener>(), + -0, + *typedArray<EventListener>() + ) +} + +/** + * @see KtLint.EDITOR_CONFIG_USER_DATA_KEY + * @see KtLint.ANDROID_USER_DATA_KEY + */ +fun get(key: String): String? + +// expect +// 2:36:Line must not end with "." +// 3:33:Line must not end with "." +// 5:19:Line must not end with "?:" +// 7:18:Line must not end with "?." +// 12:9:Line must not begin with "&&" +// 14:9:Line must not begin with "&&" +// 16:9:Line must not begin with "/" +// 22:9:Line must not begin with "+" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/max-line-length/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/max-line-length/lint.kt.spec index c55b6970..de3ebd66 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/max-line-length/lint.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/max-line-length/lint.kt.spec @@ -7,6 +7,10 @@ fun main() { println("") // too looooooooooooooooooooooooooooooooooooooooooooooooooooooong } +/** + * "https://www.google.com/search?q=ktlint&rlz=1C5CHFA_enMD736MD737&oq=ktlint+&aqs=chrome..69i57j69i60l4j69i59.1286j0j4&sourceid=chrome&ie=UTF-8" + */ + // expect // 6:1:Exceeded max line length (80) // 7:1:Exceeded max line length (80) diff --git a/ktlint-ruleset-standard/src/test/resources/spec/no-blank-line-before-rbrace/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/no-blank-line-before-rbrace/lint.kt.spec index 7113f946..d53a105e 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/no-blank-line-before-rbrace/lint.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/no-blank-line-before-rbrace/lint.kt.spec @@ -16,6 +16,6 @@ fun main() {println(""" }""")} // expect -// 3:1:Needless blank line(s) -// 6:1:Needless blank line(s) -// 10:1:Needless blank line(s) +// 3:1:Unexpected blank line(s) before "}" +// 6:1:Unexpected blank line(s) before "}" +// 10:1:Unexpected blank line(s) before "}" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format-expected.kt.spec new file mode 100644 index 00000000..8e47d2ad --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format-expected.kt.spec @@ -0,0 +1,12 @@ +@O(name = "--debug", usage = "Turn on debug output") +fun main() { + val v1 = 0 - 1 * 2 + val v2 = -0 - 1 + val v3 = v * 2 + i++ + val y = +1 + var x = 1 in 3..4 + fun <T> fn(): T {} + fun <T> List<T>.head() {} + fun List<String>.head() {} +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format.kt.spec new file mode 100644 index 00000000..407afc78 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/format.kt.spec @@ -0,0 +1,12 @@ +@O(name="--debug", usage = "Turn on debug output") +fun main() { + val v1 = 0-1*2 + val v2 = -0-1 + val v3 = v*2 + i++ + val y = +1 + var x = 1 in 3..4 + fun <T>fn(): T {} + fun <T>List<T>.head() {} + fun List<String>.head() {} +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/lint.kt.spec new file mode 100644 index 00000000..430456f1 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/op-spacing/lint.kt.spec @@ -0,0 +1,28 @@ +import a.b.* +fun main() { + val v = 0 - 1 * 2 + val v1 = 0-1*2 + val v2 = -0 - 1 + val v3 = v * 2 + i++ + val y = +1 + var x = 1 in 3..4 + val b = 1 < 2 + fun(a = true) + val res = ArrayList<LintError>() + fn(*arrayOfNulls<Any>(0 * 1)) + val a= "" + d *= 1 + call(*v) + open class A<T> { + open fun x() {} + } + class B<T> : A<T>() { + override fun x() = super<A>.x() + } +} + +// expect +// 4:15:Missing spacing around "-" +// 4:17:Missing spacing around "*" +// 14:10:Missing spacing before "=" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format-expected.kt.spec new file mode 100644 index 00000000..137f4228 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format-expected.kt.spec @@ -0,0 +1,8 @@ +fun main() { + (1..12 step 2).last == 11 + (1..12 step 2).last == 11 + (1..12 step 2).last == 11 + + (1..12 step 2).last == 11 + for (i in 1..4) print(i) +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format.kt.spec new file mode 100644 index 00000000..20136c21 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/format.kt.spec @@ -0,0 +1,8 @@ +fun main() { + (1 ..12 step 2).last == 11 + (1.. 12 step 2).last == 11 + (1 .. 12 step 2).last == 11 + + (1..12 step 2).last == 11 + for (i in 1..4) print(i) +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/lint.kt.spec new file mode 100644 index 00000000..b2336344 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/range-spacing/lint.kt.spec @@ -0,0 +1,13 @@ +fun main() { + (1 ..12 step 2).last == 11 + (1.. 12 step 2).last == 11 + (1 .. 12 step 2).last == 11 + + (1..12 step 2).last == 11 + for (i in 1..4) print(i) +} + +// expect +// 2:5:Unexpected spacing before ".." +// 3:7:Unexpected spacing after ".." +// 4:6:Unexpected spacing around ".." diff --git a/ktlint-ruleset-standard/src/test/resources/spec/string-template/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/string-template/lint.kt.spec index 3866ac1f..20e09901 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/string-template/lint.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/string-template/lint.kt.spec @@ -22,8 +22,8 @@ class B(val k: String) { } // expect -// 2:29:Redundant 'toString()' call in string template -// 3:28:Redundant 'toString()' call in string template +// 2:29:Redundant "toString()" call in string template +// 3:28:Redundant "toString()" call in string template // 6:15:Redundant curly braces // 7:15:Redundant curly braces -// 21:79:Redundant 'toString()' call in string template +// 21:79:Redundant "toString()" call in string template diff --git a/ktlint-ruleset-template/build.gradle b/ktlint-ruleset-template/build.gradle index 209230ff..3a7cbcec 100644 --- a/ktlint-ruleset-template/build.gradle +++ b/ktlint-ruleset-template/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.1.51' + ext.kotlin_version = '1.2.40' repositories { mavenCentral() maven { url 'http://repo.spring.io/plugins-release' } @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M2' + classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0' } } @@ -24,7 +24,7 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - mavenCentral() + jcenter() } task sourcesJar(type: Jar, dependsOn: classes) { @@ -48,14 +48,14 @@ configurations { dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - provided 'com.github.shyiko.ktlint:ktlint-core:0.10.0' + provided 'com.github.shyiko.ktlint:ktlint-core:0.22.0' - testCompile 'org.jetbrains.spek:spek-api:1.0.89' - testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.0.89' + testCompile 'org.jetbrains.spek:spek-api:1.1.5' + testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.1.5' testCompile 'org.assertj:assertj-core:3.5.2' - testCompile 'com.github.shyiko.ktlint:ktlint-test:0.9.0' + testCompile 'com.github.shyiko.ktlint:ktlint-test:0.22.0' - ktlint 'com.github.shyiko:ktlint:0.10.0' + ktlint 'com.github.shyiko:ktlint:0.22.0' } task ktlint(type: JavaExec, dependsOn: classes) { diff --git a/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt b/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt index 0c6b81f1..9eee6eca 100644 --- a/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt +++ b/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt @@ -8,8 +8,11 @@ import org.jetbrains.kotlin.psi.KtStringTemplateEntry class NoVarRule : Rule("no-var") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches("var") && getNonStrictParentOfType(node, KtStringTemplateEntry::class.java) == null) { emit(node.startOffset, "Unexpected var, use val instead", false) diff --git a/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt b/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt index 2692a3ea..633977dc 100644 --- a/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt +++ b/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt @@ -25,4 +25,3 @@ class NoVarRuleTest : Spek({ } } }) - diff --git a/ktlint-test/build.gradle b/ktlint-test/build.gradle new file mode 100644 index 00000000..c9d2ae31 --- /dev/null +++ b/ktlint-test/build.gradle @@ -0,0 +1,9 @@ +plugins { + id "org.jetbrains.kotlin.jvm" +} + +dependencies { + compile project(':ktlint-core') + compile libraries.kotlin_stdlib + compile libraries.kolor +} diff --git a/ktlint-test/pom.xml b/ktlint-test/pom.xml index 5a41ded4..c212142f 100644 --- a/ktlint-test/pom.xml +++ b/ktlint-test/pom.xml @@ -24,6 +24,17 @@ <version>0.0.0-SNAPSHOT</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>com.andreapivetta.kolor</groupId> + <artifactId>kolor</artifactId> + <version>${kolor.version}</version> + <exclusions> + <exclusion> + <groupId>org.jetbrains.kotlin</groupId> + <artifactId>kotlin-stdlib-jre8</artifactId> + </exclusion> + </exclusions> + </dependency> </dependencies> <build> diff --git a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt new file mode 100644 index 00000000..a22223ee --- /dev/null +++ b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt @@ -0,0 +1,94 @@ +package com.github.shyiko.ktlint.test + +import com.andreapivetta.kolor.Color +import com.andreapivetta.kolor.Kolor +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.diagnostics.DiagnosticUtils +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes +import java.io.PrintStream + +val debugAST = { + (System.getProperty("ktlintDebug") ?: System.getenv("KTLINT_DEBUG") ?: "") + .toLowerCase().split(",").contains("ast") +} + +class DumpAST @JvmOverloads constructor( + private val out: PrintStream = System.err, + private val color: Boolean = false +) : Rule("dump") { + + private var lineNumberColumnLength: Int = 0 + private var lastNode: ASTNode? = null + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit + ) { + if (node.elementType == KtStubElementTypes.FILE) { + lineNumberColumnLength = (location(PsiTreeUtil.getDeepestLast(node.psi).node)?.line ?: 1) + .let { var v = it; var c = 0; while (v > 0) { c++; v /= 10 }; c } + lastNode = lastChildNodeOf(node) + } + var level = -1 + var parent: ASTNode? = node + do { + level++ + parent = parent?.treeParent + } while (parent != null) + out.println(( + location(node) + ?.let { String.format("%${lineNumberColumnLength}s: ", it.line).gray() } + // should only happen when autoCorrect=true and other rules mutate AST in a way that changes text length + ?: String.format("%${lineNumberColumnLength}s: ", "?").gray() + ) + + " ".repeat(level).gray() + + colorClassName(node.psi.className) + + " (".gray() + colorClassName(node.elementType.className) + "." + node.elementType + ")".gray() + + if (node.getChildren(null).isEmpty()) " \"" + node.text.escape().yellow() + "\"" else "") + if (lastNode == node) { + out.println() + out.println(" ".repeat(lineNumberColumnLength) + + " format: <line_number:> <node.psi::class> (<node.elementType>) \"<node.text>\"".gray()) + out.println(" ".repeat(lineNumberColumnLength) + + " legend: ~ = org.jetbrains.kotlin, c.i.p = com.intellij.psi".gray()) + out.println() + } + } + + private tailrec fun lastChildNodeOf(node: ASTNode): ASTNode? = + if (node.lastChildNode == null) node else lastChildNodeOf(node.lastChildNode) + + private fun location(node: ASTNode) = + node.psi.containingFile?.let { psiFile -> + try { + DiagnosticUtils.getLineAndColumnInPsiFile( + psiFile, + TextRange(node.startOffset, node.startOffset) + ) + } catch (e: Exception) { + null // DiagnosticUtils has no knowledge of mutated AST + } + } + + private fun colorClassName(className: String): String { + val name = className.substringAfterLast(".") + return className.substring(0, className.length - name.length).gray() + name + } + + private fun String.yellow() = + if (color) Kolor.foreground(this, Color.YELLOW) else this + private fun String.gray() = + if (color) Kolor.foreground(this, Color.DARK_GRAY) else this + + private val Any.className + get() = this.javaClass.name + .replace("org.jetbrains.kotlin.", "~.") + .replace("com.intellij.psi.", "c.i.p.") + + private fun String.escape() = + this.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") +} diff --git a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/RuleExtension.kt b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/RuleExtension.kt index 29117900..3914705b 100644 --- a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/RuleExtension.kt +++ b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/RuleExtension.kt @@ -6,10 +6,11 @@ import com.github.shyiko.ktlint.core.Rule import com.github.shyiko.ktlint.core.RuleSet import java.util.ArrayList -fun Rule.lint(text: String, userData: Map<String, String> = emptyMap()): List<LintError> { +fun Rule.lint(text: String, userData: Map<String, String> = emptyMap(), script: Boolean = false): List<LintError> { val res = ArrayList<LintError>() val debug = debugAST() - KtLint.lint(text, (if (debug) listOf(RuleSet("debug", DumpAST())) else emptyList()) + + val f: L = if (script) KtLint::lintScript else KtLint::lint + f(text, (if (debug) listOf(RuleSet("debug", DumpAST())) else emptyList()) + listOf(RuleSet("standard", this@lint)), userData) { e -> if (debug) { System.err.println("^^ lint error") @@ -19,10 +20,27 @@ fun Rule.lint(text: String, userData: Map<String, String> = emptyMap()): List<Li return res } +private typealias L = ( + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError) -> Unit +) -> Unit + fun Rule.format( text: String, userData: Map<String, String> = emptyMap(), - cb: (e: LintError, corrected: Boolean) -> Unit = { _, _ -> } -): String = - KtLint.format(text, (if (debugAST()) listOf(RuleSet("debug", DumpAST())) else emptyList()) + + cb: (e: LintError, corrected: Boolean) -> Unit = { _, _ -> }, + script: Boolean = false +): String { + val f: F = if (script) KtLint::formatScript else KtLint::format + return f(text, (if (debugAST()) listOf(RuleSet("debug", DumpAST())) else emptyList()) + listOf(RuleSet("standard", this@format)), userData, cb) +} + +private typealias F = ( + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError, corrected: Boolean) -> Unit +) -> String diff --git a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt deleted file mode 100644 index a3c6b837..00000000 --- a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.shyiko.ktlint.test - -import com.github.shyiko.ktlint.core.Rule -import org.jetbrains.kotlin.com.intellij.lang.ASTNode - -val debugAST = { - (System.getProperty("ktlintDebug") ?: System.getenv("KTLINT_DEBUG") ?: "") - .toLowerCase().split(",").contains("ast") -} - -class DumpAST : Rule("dump") { - - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit) { - var level = -1 - var parent: ASTNode? = node - do { - level++ - parent = parent?.treeParent - } while (parent != null) - System.err.println(" ".repeat(level) + node.psi.javaClass.name + " (${node.elementType})" + - (if (node.getChildren(null).isEmpty()) " | \"" + node.text.escape() + "\"" else "")) - } - - private fun String.escape() = - this.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") -} diff --git a/ktlint/build.gradle b/ktlint/build.gradle new file mode 100644 index 00000000..c5fd4c21 --- /dev/null +++ b/ktlint/build.gradle @@ -0,0 +1,42 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "application" + // applied after mainClassName per https://github.com/johnrengelman/shadow/issues/336 + id "com.github.johnrengelman.shadow" version "2.0.2" apply false +} + +mainClassName = 'com.github.shyiko.ktlint.Main' +apply plugin: 'com.github.johnrengelman.shadow' + +dependencies { + compile project(":ktlint-core") + compile project(":ktlint-ruleset-standard") + compile project(":ktlint-reporter-plain") + compile project(":ktlint-reporter-json") + compile project(":ktlint-reporter-checkstyle") + compile libraries.kotlin_stdlib + compile libraries.klob + compile libraries.aether_api + compile libraries.aether_spi + compile libraries.aether_util + compile libraries.aether_impl + compile libraries.aether_connector_basic + compile libraries.aether_transport_file + compile libraries.aether_transport_http + compile libraries.slf4j_nop + compile libraries.maven_aether_provider + compile libraries.picocli + + testCompile libraries.testng + testCompile libraries.assertj_core + testCompile libraries.jimfs +} + +compileKotlin { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/ktlint/pom.xml b/ktlint/pom.xml index a0f95091..9b86761f 100644 --- a/ktlint/pom.xml +++ b/ktlint/pom.xml @@ -24,7 +24,15 @@ <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> + <!-- no longer included in kotlin-compiler-embeddable (>=1.1.60) --> + <!--<scope>provided</scope>--> + </dependency> + <dependency> + <groupId>org.jetbrains</groupId> + <artifactId>annotations</artifactId> + <version>13.0</version> <scope>provided</scope> + <!-- included (at least org.jetbrains.annotations.*) in kotlin-compiler-embeddable --> </dependency> <dependency> <groupId>com.github.shyiko.ktlint</groupId> @@ -52,16 +60,16 @@ <version>0.0.0-SNAPSHOT</version> </dependency> <dependency> + <groupId>com.github.shyiko.ktlint</groupId> + <artifactId>ktlint-test</artifactId> + <version>0.0.0-SNAPSHOT</version> + </dependency> + <dependency> <groupId>com.github.shyiko.klob</groupId> <artifactId>klob</artifactId> <version>0.2.0</version> </dependency> <dependency> - <groupId>org.ini4j</groupId> - <artifactId>ini4j</artifactId> - <version>0.5.4</version> - </dependency> - <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-aether-provider</artifactId> <version>${aether.maven.provider.version}</version> @@ -86,6 +94,21 @@ </dependency> <!-- maven-aether-provider's transitive dependency --> <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-model</artifactId> + <version>${aether.maven.provider.version}</version> + </dependency> + <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-model-builder</artifactId> + <version>${aether.maven.provider.version}</version> + </dependency> + <dependency> + <groupId>org.apache.maven</groupId> + <artifactId>maven-repository-metadata</artifactId> + <version>${aether.maven.provider.version}</version> + </dependency> + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> @@ -132,9 +155,9 @@ <version>1.6.2</version> </dependency> <dependency> - <groupId>args4j</groupId> - <artifactId>args4j</artifactId> - <version>${args4j.version}</version> + <groupId>info.picocli</groupId> + <artifactId>picocli</artifactId> + <version>${picocli.version}</version> </dependency> <dependency> <groupId>org.testng</groupId> @@ -148,6 +171,12 @@ <version>${assertj.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>com.google.jimfs</groupId> + <artifactId>jimfs</artifactId> + <version>${jimfs.version}</version> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -157,7 +186,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> + <version>1.8</version> <executions> <execution> <id>ktlint</id> @@ -259,7 +288,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> - <version>2.4.3</version> + <version>3.1.0</version> <executions> <execution> <phase>package</phase> @@ -335,12 +364,13 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> + <version>1.8</version> <executions> <execution> <id>ktlint</id> <phase>verify</phase> <configuration> + <skip>${gpg.skip}</skip> <target name="gpg-sign"> <exec executable="gpg" dir="${basedir}" failonerror="true"> <arg value="-ab"/> @@ -357,12 +387,13 @@ <plugin> <groupId>de.jutzig</groupId> <artifactId>github-release-plugin</artifactId> - <version>1.1.1</version> + <version>1.2.0</version> <configuration> - <description>${project.version}</description> + <description>${github.description}</description> <releaseName>${project.version}</releaseName> <tag>${project.version}</tag> <artifact>${project.build.directory}/${project.artifactId}</artifact> + <overwriteArtifact>true</overwriteArtifact> <fileSets> <fileSet> <directory>${project.build.directory}</directory> diff --git a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt index ce805632..9541cdcb 100644 --- a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt +++ b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/Main.kt @@ -12,6 +12,7 @@ import com.github.shyiko.ktlint.core.RuleSetProvider import com.github.shyiko.ktlint.internal.EditorConfig import com.github.shyiko.ktlint.internal.IntellijIDEAIntegration import com.github.shyiko.ktlint.internal.MavenDependencyResolver +import com.github.shyiko.ktlint.test.DumpAST import org.eclipse.aether.RepositoryException import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.repository.RemoteRepository @@ -19,19 +20,14 @@ import org.eclipse.aether.repository.RepositoryPolicy import org.eclipse.aether.repository.RepositoryPolicy.CHECKSUM_POLICY_IGNORE import org.eclipse.aether.repository.RepositoryPolicy.UPDATE_POLICY_NEVER import org.jetbrains.kotlin.preprocessor.mkdirsOrFail -import org.kohsuke.args4j.Argument -import org.kohsuke.args4j.CmdLineException -import org.kohsuke.args4j.CmdLineParser -import org.kohsuke.args4j.NamedOptionDef -import org.kohsuke.args4j.Option -import org.kohsuke.args4j.OptionHandlerFilter -import org.kohsuke.args4j.ParserProperties -import org.kohsuke.args4j.spi.OptionHandler +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters import java.io.ByteArrayOutputStream import java.io.File import java.io.FileNotFoundException import java.io.PrintStream -import java.io.PrintWriter import java.math.BigInteger import java.net.URLDecoder import java.nio.file.Path @@ -39,8 +35,8 @@ import java.nio.file.Paths import java.security.MessageDigest import java.util.ArrayList import java.util.Arrays +import java.util.LinkedHashMap import java.util.NoSuchElementException -import java.util.ResourceBundle import java.util.Scanner import java.util.ServiceLoader import java.util.concurrent.ArrayBlockingQueue @@ -50,8 +46,41 @@ import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import java.util.jar.Manifest import kotlin.system.exitProcess +@Command( + headerHeading = """An anti-bikeshedding Kotlin linter with built-in formatter +(https://github.com/shyiko/ktlint). + +Usage: + ktlint <flags> [patterns] + java -jar ktlint <flags> [patterns] + +Examples: + # check the style of all Kotlin files inside the current dir (recursively) + # (hidden folders will be skipped) + ktlint + + # check only certain locations (prepend ! to negate the pattern) + ktlint "src/**/*.kt" "!src/**/*Test.kt" + + # auto-correct style violations + ktlint -F "src/**/*.kt" + + # custom reporter + ktlint --reporter=plain?group_by_file + # multiple reporters can be specified like this + ktlint --reporter=plain \ + --reporter=checkstyle,output=ktlint-checkstyle-report.xml + # 3rd-party reporter + ktlint --reporter=html,artifact=com.gihub.user:repo:master-SNAPSHOT + +Flags:""", + synopsisHeading = "", + customSynopsis = arrayOf(""), + sortOptions = false +) object Main { private val DEPRECATED_FLAGS = mapOf( @@ -64,103 +93,127 @@ object Main { "--reporter-update" to "--repository-update" ) - private val CLI_MAX_LINE_LENGTH_REGEX = Regex("(.{0,120})(?:\\s|$)") + @Option(names = arrayOf("--android", "-a"), description = arrayOf("Turn on Android Kotlin Style Guide compatibility")) + private var android: Boolean = false - // todo: this should have been a command, not a flag (consider changing in 1.0.0) - @Option(name="--format", aliases = arrayOf("-F"), usage = "Fix any deviations from the code style") - private var format: Boolean = false + // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) + @Option(names = arrayOf("--apply-to-idea"), description = arrayOf("Update Intellij IDEA settings (global)")) + private var apply: Boolean = false - @Option(name="--android", aliases = arrayOf("-a"), usage = "Turn on Android Kotlin Style Guide compatibility") - private var android: Boolean = false + // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) + @Option(names = arrayOf("--apply-to-idea-project"), description = arrayOf("Update Intellij IDEA project settings")) + private var applyToProject: Boolean = false - @Option(name="--reporter", - usage = "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle). " + - "To use a third-party reporter specify either a path to a JAR file on the filesystem or a" + - "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + - "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + - "Maven Central/JCenter/JitPack/user-provided repository)") - private var reporters = ArrayList<String>() + @Option(names = arrayOf("--color"), description = arrayOf("Make output colorful")) + private var color: Boolean = false - @Option(name="--ruleset", aliases = arrayOf("-R"), - usage = "A path to a JAR file containing additional ruleset(s) or a " + - "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + - "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + - "Maven Central/JCenter/JitPack/user-provided repository)") - private var rulesets = ArrayList<String>() + @Option(names = arrayOf("--debug"), description = arrayOf("Turn on debug output")) + private var debug: Boolean = false - @Option(name="--repository", aliases = arrayOf("--ruleset-repository", "--reporter-repository"), - usage = "An additional Maven repository (Maven Central/JCenter/JitPack are active by default) " + - "(value format: <id>=<url>)") - private var repositories = ArrayList<String>() + // todo: this should have been a command, not a flag (consider changing in 1.0.0) + @Option(names = arrayOf("--format", "-F"), description = arrayOf("Fix any deviations from the code style")) + private var format: Boolean = false - @Option(name="--repository-update", aliases = arrayOf("-U", "--ruleset-update", "--reporter-update"), - usage = "Check remote repositories for updated snapshots") - private var forceUpdate: Boolean = false + @Option(names = arrayOf("--install-git-pre-commit-hook"), description = arrayOf( + "Install git hook to automatically check files for style violations on commit" + )) + private var installGitPreCommitHook: Boolean = false - @Option(name="--limit", usage = "Maximum number of errors to show (default: show all)") + @Option(names = arrayOf("--limit"), description = arrayOf( + "Maximum number of errors to show (default: show all)" + )) private var limit: Int = -1 + get() = if (field < 0) Int.MAX_VALUE else field + + @Option(names = arrayOf("--print-ast"), description = arrayOf( + "Print AST (useful when writing/debugging rules)" + )) + private var printAST: Boolean = false - @Option(name="--relative", usage = "Print files relative to the working directory " + - "(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)") + @Option(names = arrayOf("--relative"), description = arrayOf( + "Print files relative to the working directory " + + "(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)" + )) private var relative: Boolean = false - @Option(name="--verbose", aliases = arrayOf("-v"), usage = "Show error codes") - private var verbose: Boolean = false + @Option(names = arrayOf("--reporter"), description = arrayOf( + "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle). " + + "To use a third-party reporter specify either a path to a JAR file on the filesystem or a" + + "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + + "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + + "Maven Central/JCenter/JitPack/user-provided repository)\n" + + "e.g. \"html,artifact=com.github.username:ktlint-reporter-html:master-SNAPSHOT\"" + )) + private var reporters = ArrayList<String>() + + @Option(names = arrayOf("--repository"), description = arrayOf( + "An additional Maven repository (Maven Central/JCenter/JitPack are active by default) " + + "(value format: <id>=<url>)" + )) + private var repositories = ArrayList<String>() + @Option(names = arrayOf("--ruleset-repository", "--reporter-repository"), hidden = true) + private var repositoriesDeprecated = ArrayList<String>() + + @Option(names = arrayOf("--repository-update", "-U"), description = arrayOf( + "Check remote repositories for updated snapshots" + )) + private var forceUpdate: Boolean? = null + @Option(names = arrayOf("--ruleset-update", "--reporter-update"), hidden = true) + private var forceUpdateDeprecated: Boolean? = null + + @Option(names = arrayOf("--ruleset", "-R"), description = arrayOf( + "A path to a JAR file containing additional ruleset(s) or a " + + "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + + "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + + "Maven Central/JCenter/JitPack/user-provided repository)" + )) + private var rulesets = ArrayList<String>() + + @Option(names = arrayOf("--skip-classpath-check"), description = arrayOf("Do not check classpath for pottential conflicts")) + private var skipClasspathCheck: Boolean = false - @Option(name="--stdin", usage = "Read file from stdin") + @Option(names = arrayOf("--stdin"), description = arrayOf("Read file from stdin")) private var stdin: Boolean = false - @Option(name="--version", usage = "Version", help = true) + @Option(names = arrayOf("--verbose", "-v"), description = arrayOf("Show error codes")) + private var verbose: Boolean = false + + @Option(names = arrayOf("--version"), description = arrayOf("Print version information")) private var version: Boolean = false - @Option(name="--help", aliases = arrayOf("-h"), help = true) + @Option(names = arrayOf("--help", "-h"), help = true, hidden = true) private var help: Boolean = false - @Option(name="--debug", usage = "Turn on debug output") - private var debug: Boolean = false - - // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) - @Option(name="--apply-to-idea", usage = "Update Intellij IDEA project settings") - private var apply: Boolean = false - @Option(name="--install-git-pre-commit-hook", usage = "Install git hook to automatically check files for style violations on commit") - private var installGitPreCommitHook: Boolean = false - @Option(name="-y", hidden = true) + @Option(names = arrayOf("-y"), hidden = true) private var forceApply: Boolean = false - @Argument + @Parameters(hidden = true) private var patterns = ArrayList<String>() - private fun CmdLineParser.usage(): String = - """ - An anti-bikeshedding Kotlin linter with built-in formatter (https://github.com/shyiko/ktlint). - - Usage: - ktlint <flags> [patterns] - java -jar ktlint <flags> [patterns] + private val workDir = File(".").canonicalPath + private fun File.location() = if (relative) this.toRelativeString(File(workDir)) else this.path - Examples: - # check the style of all Kotlin files inside the current dir (recursively) - # (hidden folders will be skipped) - ktlint + private fun usage() = + ByteArrayOutputStream() + .also { CommandLine.usage(this, PrintStream(it), CommandLine.Help.Ansi.OFF) } + .toString() + .replace(" ".repeat(32), " ".repeat(30)) - # check only certain locations (prepend ! to negate the pattern) - ktlint "src/**/*.kt" "!src/**/*Test.kt" - - # auto-correct style violations - ktlint -F "src/**/*.kt" - - # use custom reporter - ktlint --reporter=plain?group_by_file - # multiple reporters can be specified like this - ktlint --reporter=plain --reporter=checkstyle,output=ktlint-checkstyle-report.xml - - Flags: -${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().split("\n") - .map { line -> (" " + line).replace(CLI_MAX_LINE_LENGTH_REGEX, " $1\n").trimEnd() } - .joinToString("\n")} - """.trimIndent() - - fun parseCmdLine(args: Array<String>) { + private fun parseCmdLine(args: Array<String>) { + try { + CommandLine.populateCommand(this, *args) + repositories.addAll(repositoriesDeprecated) + if (forceUpdateDeprecated != null && forceUpdate == null) { + forceUpdate = forceUpdateDeprecated + } + } catch (e: Exception) { + System.err.println("Error: ${e.message}\n\n${usage()}") + exitProcess(1) + } + if (help) { + println(usage()) + exitProcess(0) + } args.forEach { arg -> if (arg.startsWith("--") && arg.contains("=")) { val flag = arg.substringBefore("=") @@ -170,43 +223,13 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s } } } - val parser = object : CmdLineParser(this, ParserProperties.defaults() - .withShowDefaults(false) - .withUsageWidth(512) - .withOptionSorter { l, r -> - l.option.toString().replace("-", "").compareTo(r.option.toString().replace("-", "")) - }) { - - override fun printOption(out: PrintWriter, handler: OptionHandler<*>, len: Int, rb: ResourceBundle?, filter: OptionHandlerFilter?) { - handler.defaultMetaVariable - val opt = handler.option as? NamedOptionDef ?: return - if (opt.hidden() || opt.help()) { - return - } - val maxNameLength = options.map { h -> - val o = h.option - (o as? NamedOptionDef)?.let { it.name().length + 1 + (h.defaultMetaVariable ?: "").length } ?: 0 - }.max()!! - val shorthand = opt.aliases().find { it.startsWith("-") && !it.startsWith("--") } - val line = (if (shorthand != null) "$shorthand, " else " ") + - (opt.name() + " " + (handler.defaultMetaVariable ?: "")).padEnd(maxNameLength, ' ') + " " + opt.usage() - out.println(line) - } - } - try { - parser.parseArgument(*args) - } catch (err: CmdLineException) { - System.err.println("Error: ${err.message}\n\n${parser.usage()}") - exitProcess(1) - } - if (help) { println(parser.usage()); exitProcess(0) } } @JvmStatic fun main(args: Array<String>) { parseCmdLine(args) if (version) { - println(javaClass.`package`.implementationVersion) + println(getImplementationVersion()) exitProcess(0) } if (installGitPreCommitHook) { @@ -215,116 +238,59 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s exitProcess(0) } } - if (apply) { + if (apply || applyToProject) { applyToIDEA() exitProcess(0) } - val workDir = File(".").canonicalPath + if (printAST) { + printAST() + exitProcess(0) + } val start = System.currentTimeMillis() // load 3rd party ruleset(s) (if any) - val dependencyResolver by lazy { buildDependencyResolver() } + val dependencyResolver = lazy(LazyThreadSafetyMode.NONE) { buildDependencyResolver() } if (!rulesets.isEmpty()) { loadJARs(dependencyResolver, rulesets) } // standard should go first - val rp = ServiceLoader.load(RuleSetProvider::class.java) + val ruleSetProviders = ServiceLoader.load(RuleSetProvider::class.java) .map { it.get().id to it } .sortedBy { if (it.first == "standard") "\u0000${it.first}" else it.first } if (debug) { - rp.forEach { System.err.println("[DEBUG] Discovered ruleset \"${it.first}\"") } - } - data class R(val id: String, val config: Map<String, String>, var output: String?) - if (reporters.isEmpty()) { - reporters.add("plain") + ruleSetProviders.forEach { System.err.println("[DEBUG] Discovered ruleset \"${it.first}\"") } } - val rr = this.reporters.map { reporter -> - val split = reporter.split(",") - val (reporterId, rawReporterConfig) = split[0].split("?", limit = 2) + listOf("") - R(reporterId, mapOf("verbose" to verbose.toString()) + parseQuery(rawReporterConfig), - split.getOrNull(1)?.let { if (it.startsWith("output=")) it.split("=")[1] else null }) - }.distinct() - // load reporter - val reporterLoader = ServiceLoader.load(ReporterProvider::class.java) - val reporterProviderById = reporterLoader.associate { it.id to it }.let { map -> - val missingReporters = rr.map { it.id }.distinct().filter { !map.containsKey(it) } - if (!missingReporters.isEmpty()) { - loadJARs(dependencyResolver, missingReporters) - reporterLoader.reload() - reporterLoader.associate { it.id to it } - } else map - } - if (debug) { - reporterProviderById.forEach { (id) -> System.err.println("[DEBUG] Discovered reporter \"$id\"") } - } - val reporter = Reporter.from(*rr.map { r -> - val reporterProvider = reporterProviderById[r.id] - if (reporterProvider == null) { - System.err.println("Error: reporter \"${r.id}\" wasn't found (available: ${ - reporterProviderById.keys.sorted().joinToString(",")})") - exitProcess(1) - } - if (debug) { - System.err.println("[DEBUG] Initializing \"${r.id}\" reporter with ${r.config}" + - (r.output?.let { ", output=$it" } ?: "")) - } - val output = if (r.output != null) { File(r.output).parentFile?.mkdirsOrFail(); PrintStream(r.output) } else - if (stdin) System.err else System.out - reporterProvider.get(output, r.config).let { reporter -> - if (r.output != null) - object : Reporter by reporter { - override fun afterAll() { - reporter.afterAll() - output.close() - } - } - else - reporter - } - }.toTypedArray()) + val reporter = loadReporter(dependencyResolver) // load .editorconfig val userData = ( EditorConfig.of(workDir) ?.also { editorConfig -> if (debug) { - System.err.println("[DEBUG] Discovered .editorconfig (${editorConfig.path.parent})") + System.err.println("[DEBUG] Discovered .editorconfig (${ + generateSequence(editorConfig) { it.parent }.map { it.path.parent.toFile().location() }.joinToString() + })") System.err.println("[DEBUG] ${editorConfig.mapKeys { it.key }} loaded from .editorconfig") } } ?: emptyMap<String, String>() ) + mapOf("android" to android.toString()) - data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean) - fun lintErrorFrom(e: Exception): LintError = when (e) { - is ParseException -> - LintError(e.line, e.col, "", - "Not a valid Kotlin file (${e.message?.toLowerCase()})") - is RuleExecutionException -> { - if (debug) { - System.err.println("[DEBUG] Internal Error (${e.ruleId})") - e.printStackTrace(System.err) - } - LintError(e.line, e.col, "", "Internal Error (${e.ruleId}). " + - "Please create a ticket at https://github.com/shyiko/ktlint/issue " + - "(if possible, provide the source code that triggered an error)") - } - else -> throw e - } val tripped = AtomicBoolean() + data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean) fun process(fileName: String, fileContent: String): List<LintErrorWithCorrectionInfo> { if (debug) { - System.err.println("[DEBUG] Checking ${ - if (relative && fileName != "<text>") File(fileName).toRelativeString(File(workDir)) else fileName - }") + System.err.println("[DEBUG] Checking ${if (fileName != "<text>") File(fileName).location() else fileName}") } val result = ArrayList<LintErrorWithCorrectionInfo>() + val localUserData = if (fileName != "<text>") userData + ("file_path" to fileName) else userData if (format) { val formattedFileContent = try { - format(fileName, fileContent, rp.map { it.second.get() }, userData) { err, corrected -> + format(fileName, fileContent, ruleSetProviders.map { it.second.get() }, localUserData) { err, corrected -> if (!corrected) { result.add(LintErrorWithCorrectionInfo(err, corrected)) + tripped.set(true) } } } catch (e: Exception) { - result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) + result.add(LintErrorWithCorrectionInfo(e.toLintError(), false)) tripped.set(true) fileContent // making sure `cat file | ktlint --stdint > file` is (relatively) safe } @@ -337,19 +303,17 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s } } else { try { - lint(fileName, fileContent, rp.map { it.second.get() }, userData) { err -> - tripped.set(true) + lint(fileName, fileContent, ruleSetProviders.map { it.second.get() }, localUserData) { err -> result.add(LintErrorWithCorrectionInfo(err, false)) + tripped.set(true) } } catch (e: Exception) { - result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) + result.add(LintErrorWithCorrectionInfo(e.toLintError(), false)) + tripped.set(true) } } return result } - if (limit < 0) { - limit = Int.MAX_VALUE - } val (fileNumber, errorNumber) = Pair(AtomicInteger(), AtomicInteger()) fun report(fileName: String, errList: List<LintErrorWithCorrectionInfo>) { fileNumber.incrementAndGet() @@ -365,35 +329,141 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s if (stdin) { report("<text>", process("<text>", String(System.`in`.readBytes()))) } else { - val pathIterator = when { - patterns.isEmpty() -> - Glob.from("**/*.kt", "**/*.kts") - .iterate(Paths.get(workDir), Glob.IterationOption.SKIP_HIDDEN) - else -> - Glob.from(*patterns.map { expandTilde(it) }.toTypedArray()) - .iterate(Paths.get(workDir)) - } - pathIterator - .asSequence() + fileSequence() .takeWhile { errorNumber.get() < limit } - .map(Path::toFile) - .map { file -> - Callable { file to process(file.path, file.readText()) } - } - .parallel({ (file, errList) -> - report(if (relative) file.toRelativeString(File(workDir)) else file.path, errList) }) + .map { file -> Callable { file to process(file.path, file.readText()) } } + .parallel({ (file, errList) -> report(file.location(), errList) }) } reporter.afterAll() if (debug) { - System.err.println("[DEBUG] ${(System.currentTimeMillis() - start) - }ms / $fileNumber file(s) / $errorNumber error(s)") + System.err.println("[DEBUG] ${ + System.currentTimeMillis() - start + }ms / $fileNumber file(s) / $errorNumber error(s)") } if (tripped.get()) { exitProcess(1) } } - fun installGitPreCommitHook() { + private fun getImplementationVersion() = javaClass.`package`.implementationVersion + // JDK 9 regression workaround (https://bugs.openjdk.java.net/browse/JDK-8190987, fixed in JDK 10) + // (note that version reported by the fallback might not be null if META-INF/MANIFEST.MF is + // loaded from another JAR on the classpath (e.g. if META-INF/MANIFEST.MF wasn't created as part of the build)) + ?: javaClass.getResourceAsStream("/META-INF/MANIFEST.MF") + ?.let { stream -> + Manifest(stream).mainAttributes.getValue("Implementation-Version") + } + + private fun loadReporter(dependencyResolver: Lazy<MavenDependencyResolver>): Reporter { + data class ReporterTemplate(val id: String, val artifact: String?, val config: Map<String, String>, var output: String?) + val tpls = (if (reporters.isEmpty()) listOf("plain") else reporters) + .map { reporter -> + val split = reporter.split(",") + val (reporterId, rawReporterConfig) = split[0].split("?", limit = 2) + listOf("") + ReporterTemplate( + reporterId, + split.lastOrNull { it.startsWith("artifact=") }?.let { it.split("=")[1] }, + mapOf("verbose" to verbose.toString(), "color" to color.toString()) + parseQuery(rawReporterConfig), + split.lastOrNull { it.startsWith("output=") }?.let { it.split("=")[1] } + ) + } + .distinct() + val reporterLoader = ServiceLoader.load(ReporterProvider::class.java) + val reporterProviderById = reporterLoader.associate { it.id to it }.let { map -> + val missingReporters = tpls.filter { !map.containsKey(it.id) }.mapNotNull { it.artifact }.distinct() + if (!missingReporters.isEmpty()) { + loadJARs(dependencyResolver, missingReporters) + reporterLoader.reload() + reporterLoader.associate { it.id to it } + } else map + } + if (debug) { + reporterProviderById.forEach { (id) -> System.err.println("[DEBUG] Discovered reporter \"$id\"") } + } + fun ReporterTemplate.toReporter(): Reporter { + val reporterProvider = reporterProviderById[id] + if (reporterProvider == null) { + System.err.println("Error: reporter \"$id\" wasn't found (available: ${ + reporterProviderById.keys.sorted().joinToString(",") + })") + exitProcess(1) + } + if (debug) { + System.err.println("[DEBUG] Initializing \"$id\" reporter with $config" + + (output?.let { ", output=$it" } ?: "")) + } + val stream = if (output != null) { + File(output).parentFile?.mkdirsOrFail(); PrintStream(output, "UTF-8") + } else if (stdin) System.err else System.out + return reporterProvider.get(stream, config) + .let { reporter -> + if (output != null) + object : Reporter by reporter { + override fun afterAll() { + reporter.afterAll() + stream.close() + } + } + else reporter + } + } + return Reporter.from(*tpls.map { it.toReporter() }.toTypedArray()) + } + + private fun Exception.toLintError(): LintError = this.let { e -> + when (e) { + is ParseException -> + LintError(e.line, e.col, "", + "Not a valid Kotlin file (${e.message?.toLowerCase()})") + is RuleExecutionException -> { + if (debug) { + System.err.println("[DEBUG] Internal Error (${e.ruleId})") + e.printStackTrace(System.err) + } + LintError(e.line, e.col, "", "Internal Error (${e.ruleId}). " + + "Please create a ticket at https://github.com/shyiko/ktlint/issue " + + "(if possible, provide the source code that triggered an error)") + } + else -> throw e + } + } + + private fun printAST() { + fun process(fileName: String, fileContent: String) { + if (debug) { + System.err.println("[DEBUG] Analyzing ${if (fileName != "<text>") File(fileName).location() else fileName}") + } + try { + lint(fileName, fileContent, listOf(RuleSet("debug", DumpAST(System.out, color))), emptyMap()) {} + } catch (e: Exception) { + if (e is ParseException) { + throw ParseException(e.line, e.col, "Not a valid Kotlin file (${e.message?.toLowerCase()})") + } + throw e + } + } + if (stdin) { + process("<text>", String(System.`in`.readBytes())) + } else { + for (file in fileSequence()) { + process(file.path, file.readText()) + } + } + } + + private fun fileSequence() = + when { + patterns.isEmpty() -> + Glob.from("**/*.kt", "**/*.kts") + .iterate(Paths.get(workDir), Glob.IterationOption.SKIP_HIDDEN) + else -> + Glob.from(*patterns.map { expandTilde(it) }.toTypedArray()) + .iterate(Paths.get(workDir)) + } + .asSequence() + .map(Path::toFile) + + private fun installGitPreCommitHook() { if (!File(".git").isDirectory) { System.err.println(".git directory not found. " + "Are you sure you are inside project root directory?") @@ -417,17 +487,16 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s System.err.println(".git/hooks/pre-commit installed") } - fun applyToIDEA() { + private fun applyToIDEA() { try { val workDir = Paths.get(".") if (!forceApply) { - val fileList = IntellijIDEAIntegration.apply(workDir, true, android) + val fileList = IntellijIDEAIntegration.apply(workDir, true, android, applyToProject) System.err.println("The following files are going to be updated:\n\n\t" + fileList.joinToString("\n\t") + "\n\nDo you wish to proceed? [y/n]\n" + "(in future, use -y flag if you wish to skip confirmation)") val scanner = Scanner(System.`in`) - val res = generateSequence { try { scanner.next() } catch (e: NoSuchElementException) { null } } @@ -438,7 +507,7 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s exitProcess(1) } } - IntellijIDEAIntegration.apply(workDir, false, android) + IntellijIDEAIntegration.apply(workDir, false, android, applyToProject) } catch (e: IntellijIDEAIntegration.ProjectNotFoundException) { System.err.println(".idea directory not found. " + "Are you sure you are inside project root directory?") @@ -449,15 +518,15 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s System.err.println("(if you experience any issues please report them at https://github.com/shyiko/ktlint)") } - fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) + private fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) // a complete solution would be to implement https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html // this implementation takes care only of the most commonly used case (~/) - fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) + private fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) - fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit) + private fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit) - fun buildDependencyResolver(): MavenDependencyResolver { + private fun buildDependencyResolver(): MavenDependencyResolver { val mavenLocal = File(File(System.getProperty("user.home"), ".m2"), "repository") mavenLocal.mkdirsOrFail() val dependencyResolver = MavenDependencyResolver( @@ -482,7 +551,7 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s val url = repository.substring(colon + 1) RemoteRepository.Builder(id, "default", url).build() }, - forceUpdate + forceUpdate == true ) if (debug) { dependencyResolver.setTransferEventListener { e -> @@ -493,14 +562,15 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s return dependencyResolver } - fun loadJARs(dependencyResolver: MavenDependencyResolver, artifacts: List<String>) { + // fixme: isn't going to work on JDK 9 + private fun loadJARs(dependencyResolver: Lazy<MavenDependencyResolver>, artifacts: List<String>) { (ClassLoader.getSystemClassLoader() as java.net.URLClassLoader) .addURLs(artifacts.flatMap { artifact -> if (debug) { System.err.println("[DEBUG] Resolving $artifact") } val result = try { - dependencyResolver.resolve(DefaultArtifact(artifact)).map { it.toURI().toURL() } + dependencyResolver.value.resolve(DefaultArtifact(artifact)).map { it.toURI().toURL() } } catch (e: IllegalArgumentException) { val file = File(expandTilde(artifact)) if (!file.exists()) { @@ -518,11 +588,25 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s if (debug) { result.forEach { url -> System.err.println("[DEBUG] Loading $url") } } + if (!skipClasspathCheck) { + if (result.any { it.toString().substringAfterLast("/").startsWith("ktlint-core-") }) { + System.err.println("\"$artifact\" appears to have a runtime/compile dependency on \"ktlint-core\".\n" + + "Please inform the author that \"com.github.shyiko:ktlint*\" should be marked " + + "compileOnly (Gradle) / provided (Maven).\n" + + "(to suppress this warning use --skip-classpath-check)") + } + if (result.any { it.toString().substringAfterLast("/").startsWith("kotlin-stdlib-") }) { + System.err.println("\"$artifact\" appears to have a runtime/compile dependency on \"kotlin-stdlib\".\n" + + "Please inform the author that \"org.jetbrains.kotlin:kotlin-stdlib*\" should be marked " + + "compileOnly (Gradle) / provided (Maven).\n" + + "(to suppress this warning use --skip-classpath-check)") + } + } result }) } - fun parseQuery(query: String) = query.split("&") + private fun parseQuery(query: String) = query.split("&") .fold(LinkedHashMap<String, String>()) { map, s -> if (!s.isEmpty()) { s.split("=", limit = 2).let { e -> map.put(e[0], @@ -531,24 +615,42 @@ ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().s map } - fun lint(fileName: String, text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, - cb: (e: LintError) -> Unit) = - if (fileName.endsWith(".kt", ignoreCase = true)) KtLint.lint(text, ruleSets, userData, cb) else + private fun lint( + fileName: String, + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError) -> Unit + ) = + if (fileName.endsWith(".kt", ignoreCase = true)) { + KtLint.lint(text, ruleSets, userData, cb) + } else { KtLint.lintScript(text, ruleSets, userData, cb) + } - fun format(fileName: String, text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, - cb: (e: LintError, corrected: Boolean) -> Unit): String = - if (fileName.endsWith(".kt", ignoreCase = true)) KtLint.format(text, ruleSets, userData, cb) else + private fun format( + fileName: String, + text: String, + ruleSets: Iterable<RuleSet>, + userData: Map<String, String>, + cb: (e: LintError, corrected: Boolean) -> Unit + ): String = + if (fileName.endsWith(".kt", ignoreCase = true)) { + KtLint.format(text, ruleSets, userData, cb) + } else { KtLint.formatScript(text, ruleSets, userData, cb) + } - fun java.net.URLClassLoader.addURLs(url: Iterable<java.net.URL>) { + private fun java.net.URLClassLoader.addURLs(url: Iterable<java.net.URL>) { val method = java.net.URLClassLoader::class.java.getDeclaredMethod("addURL", java.net.URL::class.java) method.isAccessible = true url.forEach { method.invoke(this, it) } } - fun <T>Sequence<Callable<T>>.parallel(cb: (T) -> Unit, - numberOfThreads: Int = Runtime.getRuntime().availableProcessors()) { + private fun <T> Sequence<Callable<T>>.parallel( + cb: (T) -> Unit, + numberOfThreads: Int = Runtime.getRuntime().availableProcessors() + ) { val q = ArrayBlockingQueue<Future<T>>(numberOfThreads) val pill = object : Future<T> { diff --git a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/EditorConfig.kt b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/EditorConfig.kt index 519f6a59..bce07c5a 100644 --- a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/EditorConfig.kt +++ b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/EditorConfig.kt @@ -1,12 +1,13 @@ package com.github.shyiko.ktlint.internal -import org.ini4j.Wini import java.io.ByteArrayInputStream import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.util.Properties class EditorConfig private constructor ( + val parent: EditorConfig?, val path: Path, private val data: Map<String, String> ) : Map<String, String> by data { @@ -16,21 +17,113 @@ class EditorConfig private constructor ( fun of(dir: String) = of(Paths.get(dir)) fun of(dir: Path) = - locate(dir)?.let { EditorConfig(it, load(it)) } + generateSequence(locate(dir)) { seed -> locate(seed.parent.parent) } // seed.parent == .editorconfig dir + .map { it to lazy { load(it) } } + .let { seq -> + // stop when .editorconfig with "root = true" is found, go deeper otherwise + var prev: Pair<Path, Lazy<Map<String, Map<String, String>>>>? = null + seq.takeWhile { pair -> + (prev?.second?.value?.get("")?.get("root")?.toBoolean()?.not() ?: true).also { prev = pair } + } + } + .toList() + .asReversed() + .fold(null as EditorConfig?) { parent, (path, data) -> + EditorConfig(parent, path, (parent?.data ?: emptyMap()) + flatten(data.value)) + } private fun locate(dir: Path?): Path? = when (dir) { null -> null - else -> Paths.get(dir.toString(), ".editorconfig").let { + else -> dir.resolve(".editorconfig").let { if (Files.exists(it)) it else locate(dir.parent) } } - private fun load(path: Path): Map<String, String> { - val editorConfig = Wini(ByteArrayInputStream(Files.readAllBytes(path))) - // right now ktlint requires explicit [*.{kt,kts}] section - // (this way we can be sure that users want .editorconfig to be recognized by ktlint) - val section = editorConfig["*.{kt,kts}"] - return section?.toSortedMap() ?: emptyMap() + private fun flatten(data: LinkedHashMap<String, Map<String, String>>): Map<String, String> { + val map = mutableMapOf<String, String>() + val patternsToSearchFor = arrayOf("*", "*.kt", "*.kts") + for ((sectionName, section) in data) { + if (sectionName == "") { + continue + } + val patterns = try { + parseSection(sectionName.substring(1, sectionName.length - 1)) + } catch (e: Exception) { + throw RuntimeException("ktlint failed to parse .editorconfig section \"$sectionName\"" + + " (please report at https://github.com/shyiko/ktlint)", e) + } + if (patternsToSearchFor.any { patterns.contains(it) }) { + map.putAll(section.toMap()) + } + } + return map.toSortedMap() + } + + private fun load(path: Path) = + linkedMapOf<String, Map<String, String>>().also { map -> + object : Properties() { + + private var section: MutableMap<String, String>? = null + + override fun put(key: Any, value: Any): Any? { + val sectionName = (key as String).trim() + if (sectionName.startsWith('[') && sectionName.endsWith(']') && value == "") { + section = mutableMapOf<String, String>().also { map.put(sectionName, it) } + } else { + val section = section + ?: mutableMapOf<String, String>().also { section = it; map.put("", it) } + section[key] = value.toString() + } + return null + } + }.load(ByteArrayInputStream(Files.readAllBytes(path))) + } + + internal fun parseSection(sectionName: String): List<String> { + val result = mutableListOf<String>() + fun List<List<String>>.collect0(i: Int = 0, str: Array<String?>, acc: MutableList<String>) { + if (i == str.size) { + acc.add(str.joinToString("")) + return + } + for (k in 0 until this[i].size) { + str[i] = this[i][k] + collect0(i + 1, str, acc) + } + } + // [["*.kt"], ["", "s"], ["~"]] -> [*.kt~, *.kts~] + fun List<List<String>>.collect(): List<String> = + mutableListOf<String>().also { this.collect0(0, arrayOfNulls<String>(this.size), it) } + val chunks: MutableList<MutableList<String>> = mutableListOf() + chunks.add(mutableListOf()) + var l = 0 + var r = 0 + var partOfBraceExpansion = false + for (c in sectionName) { + when (c) { + ',' -> { + chunks.last().add(sectionName.substring(l, r)) + l = r + 1 + if (!partOfBraceExpansion) { + result += chunks.collect() + chunks.clear() + chunks.add(mutableListOf()) + } + } + '{', '}' -> { + if (partOfBraceExpansion == (c == '}')) { + chunks.last().add(sectionName.substring(l, r)) + l = r + 1 + chunks.add(mutableListOf()) + partOfBraceExpansion = !partOfBraceExpansion + } + } + } + r++ + } + chunks.last().add(sectionName.substring(l, r)) + result += chunks.collect() + return result } } } diff --git a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/IntellijIDEAIntegration.kt b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/IntellijIDEAIntegration.kt index 7c7b23fb..0e3cc9f1 100644 --- a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/IntellijIDEAIntegration.kt +++ b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/IntellijIDEAIntegration.kt @@ -20,72 +20,103 @@ import javax.xml.xpath.XPathFactory object IntellijIDEAIntegration { + @Suppress("UNUSED_PARAMETER") @Throws(IOException::class) - fun apply(workDir: Path, dryRun: Boolean, android: Boolean = false): Array<Path> { + fun apply(workDir: Path, dryRun: Boolean, android: Boolean = false, local: Boolean = false): Array<Path> { if (!Files.isDirectory(workDir.resolve(".idea"))) { throw ProjectNotFoundException() } - val home = System.getProperty("user.home") val editorConfig: Map<String, String> = EditorConfig.of(".") ?: emptyMap() - val continuationIndentSize = editorConfig["continuation_indent_size"]?.toIntOrNull() ?: if (android) 8 else 4 val indentSize = editorConfig["indent_size"]?.toIntOrNull() ?: 4 - val codeStyleName = "ktlint${ - if (continuationIndentSize == 4) "" else "-cis$continuationIndentSize" - }${ - if (indentSize == 4) "" else "-is$indentSize" - }" - val paths = - // macOS - Glob.from("IntelliJIdea*", "IdeaIC*", "AndroidStudio*") - .iterate(Paths.get(home, "Library", "Preferences"), - Glob.IterationOption.SKIP_CHILDREN, Glob.IterationOption.DIRECTORY).asSequence() + - // linux/windows - Glob.from(".IntelliJIdea*/config", ".IdeaIC*/config", ".AndroidStudio*/config") - .iterate(Paths.get(home), - Glob.IterationOption.SKIP_CHILDREN, Glob.IterationOption.DIRECTORY).asSequence() - val updates = (paths.flatMap { dir -> - sequenceOf( - Paths.get(dir.toString(), "codestyles", "$codeStyleName.xml") to - overwriteWithResource("/config/codestyles/ktlint.xml") { resource -> + val continuationIndentSize = editorConfig["continuation_indent_size"]?.toIntOrNull() ?: 4 + val updates = if (local) { + listOf( + Paths.get(workDir.toString(), ".idea", "codeStyles", "codeStyleConfig.xml") to + overwriteWithResource("/project-config/.idea/codeStyles/codeStyleConfig.xml"), + Paths.get(workDir.toString(), ".idea", "codeStyles", "Project.xml") to + overwriteWithResource("/project-config/.idea/codeStyles/Project.xml") { resource -> resource - .replace("code_scheme name=\"ktlint\"", - "code_scheme name=\"$codeStyleName\"") .replace("option name=\"INDENT_SIZE\" value=\"4\"", "option name=\"INDENT_SIZE\" value=\"$indentSize\"") .replace("option name=\"CONTINUATION_INDENT_SIZE\" value=\"8\"", "option name=\"CONTINUATION_INDENT_SIZE\" value=\"$continuationIndentSize\"") }, - Paths.get(dir.toString(), "options", "code.style.schemes.xml") to - overwriteWithResource("/config/options/code.style.schemes.xml") { content -> - content - .replace("option name=\"CURRENT_SCHEME_NAME\" value=\"ktlint\"", - "option name=\"CURRENT_SCHEME_NAME\" value=\"$codeStyleName\"") - }, - Paths.get(dir.toString(), "inspection", "ktlint.xml") to - overwriteWithResource("/config/inspection/ktlint.xml"), - Paths.get(dir.toString(), "options", "editor.codeinsight.xml") to { - var arr = "<application></application>".toByteArray() + Paths.get(workDir.toString(), ".idea", "inspectionProfiles", "profiles_settings.xml") to + overwriteWithResource("/project-config/.idea/inspectionProfiles/profiles_settings.xml"), + Paths.get(workDir.toString(), ".idea", "inspectionProfiles", "ktlint.xml") to + overwriteWithResource("/project-config/.idea/inspectionProfiles/ktlint.xml"), + Paths.get(workDir.toString(), ".idea", "workspace.xml") to { + var arr = "<project version=\"4\"></project>".toByteArray() try { - arr = Files.readAllBytes(Paths.get(dir.toString(), "options", "editor.codeinsight.xml")) + arr = Files.readAllBytes(Paths.get(workDir.toString(), ".idea", "workspace.xml")) } catch (e: IOException) { if (e !is NoSuchFileException) { throw e } } - enableOptimizeImportsOnTheFly(arr) + enableOptimizeImportsOnTheFlyInsideWorkspace(arr) } ) - } + sequenceOf( - Paths.get(workDir.toString(), ".idea", "codeStyleSettings.xml") to - overwriteWithResource("/config/.idea/codeStyleSettings.xml") { content -> - content.replace( - "option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"ktlint\"", - "option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"$codeStyleName\"" - ) - }, - Paths.get(workDir.toString(), ".idea", "inspectionProfiles", "profiles_settings.xml") to - overwriteWithResource("/config/.idea/inspectionProfiles/profiles_settings.xml") - )).toList() + } else { + val home = System.getProperty("user.home") + val codeStyleName = "ktlint${ + if (continuationIndentSize == 4) "" else "-cis$continuationIndentSize" + }${ + if (indentSize == 4) "" else "-is$indentSize" + }" + val paths = + // macOS + Glob.from("IntelliJIdea*", "IdeaIC*", "AndroidStudio*") + .iterate(Paths.get(home, "Library", "Preferences"), + Glob.IterationOption.SKIP_CHILDREN, Glob.IterationOption.DIRECTORY).asSequence() + + // linux/windows + Glob.from(".IntelliJIdea*/config", ".IdeaIC*/config", ".AndroidStudio*/config") + .iterate(Paths.get(home), + Glob.IterationOption.SKIP_CHILDREN, Glob.IterationOption.DIRECTORY).asSequence() + (paths.flatMap { dir -> + sequenceOf( + Paths.get(dir.toString(), "codestyles", "$codeStyleName.xml") to + overwriteWithResource("/config/codestyles/ktlint.xml") { resource -> + resource + .replace("code_scheme name=\"ktlint\"", + "code_scheme name=\"$codeStyleName\"") + .replace("option name=\"INDENT_SIZE\" value=\"4\"", + "option name=\"INDENT_SIZE\" value=\"$indentSize\"") + .replace("option name=\"CONTINUATION_INDENT_SIZE\" value=\"8\"", + "option name=\"CONTINUATION_INDENT_SIZE\" value=\"$continuationIndentSize\"") + }, + Paths.get(dir.toString(), "options", "code.style.schemes.xml") to + overwriteWithResource("/config/options/code.style.schemes.xml") { content -> + content + .replace("option name=\"CURRENT_SCHEME_NAME\" value=\"ktlint\"", + "option name=\"CURRENT_SCHEME_NAME\" value=\"$codeStyleName\"") + }, + Paths.get(dir.toString(), "inspection", "ktlint.xml") to + overwriteWithResource("/config/inspection/ktlint.xml"), + Paths.get(dir.toString(), "options", "editor.codeinsight.xml") to { + var arr = "<application></application>".toByteArray() + try { + arr = Files.readAllBytes(Paths.get(dir.toString(), "options", "editor.codeinsight.xml")) + } catch (e: IOException) { + if (e !is NoSuchFileException) { + throw e + } + } + enableOptimizeImportsOnTheFly(arr) + } + ) + } + sequenceOf( + Paths.get(workDir.toString(), ".idea", "codeStyleSettings.xml") to + overwriteWithResource("/config/.idea/codeStyleSettings.xml") { content -> + content.replace( + "option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"ktlint\"", + "option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"$codeStyleName\"" + ) + }, + Paths.get(workDir.toString(), ".idea", "inspectionProfiles", "profiles_settings.xml") to + overwriteWithResource("/config/.idea/inspectionProfiles/profiles_settings.xml") + )).toList() + } if (!dryRun) { updates.forEach { (path, contentSupplier) -> Files.createDirectories(path.parent) @@ -133,6 +164,39 @@ object IntellijIDEAIntegration { return out.toByteArray() } + private fun enableOptimizeImportsOnTheFlyInsideWorkspace(arr: ByteArray): ByteArray { + /* + <project> + <component name="CodeInsightWorkspaceSettings"> + <option name="optimizeImportsOnTheFly" value="false" /> + ... + </component> + ... + </project> + */ + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ByteArrayInputStream(arr)) + val xpath = XPathFactory.newInstance().newXPath() + var cis = xpath.evaluate("//component[@name='CodeInsightWorkspaceSettings']", + doc, XPathConstants.NODE) as Element? + if (cis == null) { + cis = doc.createElement("component") + cis.setAttribute("name", "CodeInsightWorkspaceSettings") + cis = doc.documentElement.appendChild(cis) as Element + } + var oiotf = xpath.evaluate("//option[@name='optimizeImportsOnTheFly']", + cis, XPathConstants.NODE) as Element? + if (oiotf == null) { + oiotf = doc.createElement("option") + oiotf.setAttribute("name", "optimizeImportsOnTheFly") + oiotf = cis.appendChild(oiotf) as Element + } + oiotf.setAttribute("value", "true") + val transformer = TransformerFactory.newInstance().newTransformer() + val out = ByteArrayOutputStream() + transformer.transform(DOMSource(doc), StreamResult(out)) + return out.toByteArray() + } + private fun getResourceText(name: String) = this::class.java.getResourceAsStream(name).readBytes().toString(Charset.forName("UTF-8")) diff --git a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/MavenDependencyResolver.kt b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/MavenDependencyResolver.kt index a01f015f..1760279f 100644 --- a/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/MavenDependencyResolver.kt +++ b/ktlint/src/main/kotlin/com/github/shyiko/ktlint/internal/MavenDependencyResolver.kt @@ -8,6 +8,7 @@ import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.collection.CollectRequest import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.impl.DefaultServiceLocator import org.eclipse.aether.repository.LocalRepository import org.eclipse.aether.repository.RemoteRepository import org.eclipse.aether.repository.RepositoryPolicy @@ -21,8 +22,11 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator import java.io.File -class MavenDependencyResolver(baseDir: File, val repositories: Iterable<RemoteRepository>, - forceUpdate: Boolean) { +class MavenDependencyResolver( + baseDir: File, + val repositories: Iterable<RemoteRepository>, + forceUpdate: Boolean +) { private val repoSystem: RepositorySystem private val session: RepositorySystemSession @@ -32,11 +36,19 @@ class MavenDependencyResolver(baseDir: File, val repositories: Iterable<RemoteRe locator.addService(RepositoryConnectorFactory::class.java, BasicRepositoryConnectorFactory::class.java) locator.addService(TransporterFactory::class.java, FileTransporterFactory::class.java) locator.addService(TransporterFactory::class.java, HttpTransporterFactory::class.java) + locator.setErrorHandler(object : DefaultServiceLocator.ErrorHandler() { + override fun serviceCreationFailed(type: Class<*>?, impl: Class<*>?, ex: Throwable) { + throw ex + } + }) repoSystem = locator.getService(RepositorySystem::class.java) session = MavenRepositorySystemUtils.newSession() session.localRepositoryManager = repoSystem.newLocalRepositoryManager(session, LocalRepository(baseDir)) - session.updatePolicy = if (forceUpdate) RepositoryPolicy.UPDATE_POLICY_ALWAYS else + session.updatePolicy = if (forceUpdate) { + RepositoryPolicy.UPDATE_POLICY_ALWAYS + } else { RepositoryPolicy.UPDATE_POLICY_NEVER + } } fun setTransferEventListener(listener: (event: TransferEvent) -> Unit) { diff --git a/ktlint/src/main/resources/config/codestyles/ktlint.xml b/ktlint/src/main/resources/config/codestyles/ktlint.xml index 213a17db..e73bb3ba 100644 --- a/ktlint/src/main/resources/config/codestyles/ktlint.xml +++ b/ktlint/src/main/resources/config/codestyles/ktlint.xml @@ -7,8 +7,10 @@ </option> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> <codeStyleSettings language="kotlin"> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> diff --git a/ktlint/src/main/resources/config/inspection/ktlint.xml b/ktlint/src/main/resources/config/inspection/ktlint.xml index a1ad97eb..449db9e1 100644 --- a/ktlint/src/main/resources/config/inspection/ktlint.xml +++ b/ktlint/src/main/resources/config/inspection/ktlint.xml @@ -3,6 +3,5 @@ <option name="myName" value="ktlint" /> <inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="UnusedSymbol" enabled="true" level="ERROR" enabled_by_default="true" /> </inspections> diff --git a/ktlint/src/main/resources/ktlint-git-pre-commit-hook-android.sh b/ktlint/src/main/resources/ktlint-git-pre-commit-hook-android.sh index 3d8432cc..58b3994e 100755 --- a/ktlint/src/main/resources/ktlint-git-pre-commit-hook-android.sh +++ b/ktlint/src/main/resources/ktlint-git-pre-commit-hook-android.sh @@ -1,4 +1,4 @@ #!/bin/sh # https://github.com/shyiko/ktlint pre-commit hook -git diff --name-only --cached --relative | grep '\.kts\?$' | xargs ktlint --android --relative . +git diff --name-only --cached --relative | grep '\.kt[s"]\?$' | xargs ktlint --android --relative . if [ $? -ne 0 ]; then exit 1; fi diff --git a/ktlint/src/main/resources/ktlint-git-pre-commit-hook.sh b/ktlint/src/main/resources/ktlint-git-pre-commit-hook.sh index dcc2485f..7a25ea86 100755 --- a/ktlint/src/main/resources/ktlint-git-pre-commit-hook.sh +++ b/ktlint/src/main/resources/ktlint-git-pre-commit-hook.sh @@ -1,4 +1,4 @@ #!/bin/sh # https://github.com/shyiko/ktlint pre-commit hook -git diff --name-only --cached --relative | grep '\.kts\?$' | xargs ktlint --relative . +git diff --name-only --cached --relative | grep '\.kt[s"]\?$' | xargs ktlint --relative . if [ $? -ne 0 ]; then exit 1; fi diff --git a/ktlint/src/main/resources/project-config/.idea/codeStyles/Project.xml b/ktlint/src/main/resources/project-config/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..c8d67de2 --- /dev/null +++ b/ktlint/src/main/resources/project-config/.idea/codeStyles/Project.xml @@ -0,0 +1,25 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <JetCodeStyleSettings> + <option name="PACKAGES_TO_USE_STAR_IMPORTS"> + <value> + <package name="kotlinx.android.synthetic" withSubpackages="true" static="false" /> + </value> + </option> + <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> + <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + </JetCodeStyleSettings> + <codeStyleSettings language="kotlin"> + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> + <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> + <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> + <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <indentOptions> + <option name="INDENT_SIZE" value="4" /> + <option name="CONTINUATION_INDENT_SIZE" value="8" /> + </indentOptions> + </codeStyleSettings> + </code_scheme> +</component> diff --git a/ktlint/src/main/resources/project-config/.idea/codeStyles/codeStyleConfig.xml b/ktlint/src/main/resources/project-config/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..0f7bc519 --- /dev/null +++ b/ktlint/src/main/resources/project-config/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ +<component name="ProjectCodeStyleConfiguration"> + <state> + <option name="USE_PER_PROJECT_SETTINGS" value="true" /> + </state> +</component> diff --git a/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/ktlint.xml b/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/ktlint.xml new file mode 100644 index 00000000..7d04a74b --- /dev/null +++ b/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/ktlint.xml @@ -0,0 +1,7 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="ktlint" /> + <inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" /> + <inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> + </profile> +</component> diff --git a/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/profiles_settings.xml b/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..64580d14 --- /dev/null +++ b/ktlint/src/main/resources/project-config/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="PROJECT_PROFILE" value="ktlint" /> + <version value="1.0" /> + </settings> +</component> diff --git a/ktlint/src/test/kotlin/com/github/shyiko/ktlint/internal/EditorConfigTest.kt b/ktlint/src/test/kotlin/com/github/shyiko/ktlint/internal/EditorConfigTest.kt new file mode 100644 index 00000000..eb657093 --- /dev/null +++ b/ktlint/src/test/kotlin/com/github/shyiko/ktlint/internal/EditorConfigTest.kt @@ -0,0 +1,109 @@ +package com.github.shyiko.ktlint.internal + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test +import java.nio.file.Files + +class EditorConfigTest { + + @Test + fun testParentDirectoryFallback() { + val fs = Jimfs.newFileSystem(Configuration.unix()) + Files.createDirectories(fs.getPath("/projects/project-1/project-1-subdirectory")) + for (cfg in arrayOf( + """ + [*] + indent_size = 2 + """, + """ + root = true + [*] + indent_size = 2 + """, + """ + [*] + indent_size = 4 + [*.{kt,kts}] + indent_size = 2 + """, + """ + [*.{kt,kts}] + indent_size = 4 + [*] + indent_size = 2 + """ + )) { + Files.write(fs.getPath("/projects/project-1/.editorconfig"), cfg.trimIndent().toByteArray()) + val editorConfig = EditorConfig.of(fs.getPath("/projects/project-1/project-1-subdirectory")) + assertThat(editorConfig?.parent).isNull() + assertThat(editorConfig?.toMap()) + .overridingErrorMessage("Expected \n%s\nto yield indent_size = 2", cfg.trimIndent()) + .isEqualTo(mapOf("indent_size" to "2")) + } + } + + @Test + fun testRootTermination() { + val fs = Jimfs.newFileSystem(Configuration.unix()) + Files.createDirectories(fs.getPath("/projects/project-1/project-1-subdirectory")) + Files.write(fs.getPath("/projects/.editorconfig"), """ + root = true + [*] + end_of_line = lf + """.trimIndent().toByteArray()) + Files.write(fs.getPath("/projects/project-1/.editorconfig"), """ + root = true + [*.{kt,kts}] + indent_size = 4 + indent_style = space + """.trimIndent().toByteArray()) + Files.write(fs.getPath("/projects/project-1/project-1-subdirectory/.editorconfig"), """ + [*] + indent_size = 2 + """.trimIndent().toByteArray()) + EditorConfig.of(fs.getPath("/projects/project-1/project-1-subdirectory")).let { editorConfig -> + assertThat(editorConfig?.parent).isNotNull() + assertThat(editorConfig?.parent?.parent).isNull() + assertThat(editorConfig?.toMap()).isEqualTo(mapOf( + "indent_size" to "2", + "indent_style" to "space" + )) + } + EditorConfig.of(fs.getPath("/projects/project-1")).let { editorConfig -> + assertThat(editorConfig?.parent).isNull() + assertThat(editorConfig?.toMap()).isEqualTo(mapOf( + "indent_size" to "4", + "indent_style" to "space" + )) + } + EditorConfig.of(fs.getPath("/projects")).let { editorConfig -> + assertThat(editorConfig?.parent).isNull() + assertThat(editorConfig?.toMap()).isEqualTo(mapOf( + "end_of_line" to "lf" + )) + } + } + + @Test + fun testSectionParsing() { + assertThat(EditorConfig.parseSection("*")).isEqualTo(listOf("*")) + assertThat(EditorConfig.parseSection("*.{js,py}")).isEqualTo(listOf("*.js", "*.py")) + assertThat(EditorConfig.parseSection("*.py")).isEqualTo(listOf("*.py")) + assertThat(EditorConfig.parseSection("Makefile")).isEqualTo(listOf("Makefile")) + assertThat(EditorConfig.parseSection("lib/**.js")).isEqualTo(listOf("lib/**.js")) + assertThat(EditorConfig.parseSection("{package.json,.travis.yml}")) + .isEqualTo(listOf("package.json", ".travis.yml")) + } + + @Test + fun testMalformedSectionParsing() { + assertThat(EditorConfig.parseSection("")).isEqualTo(listOf("")) + assertThat(EditorConfig.parseSection(",*")).isEqualTo(listOf("", "*")) + assertThat(EditorConfig.parseSection("*,")).isEqualTo(listOf("*", "")) + assertThat(EditorConfig.parseSection("*.{js,py")).isEqualTo(listOf("*.js", "*.py")) + assertThat(EditorConfig.parseSection("*.{js,{py")).isEqualTo(listOf("*.js", "*.{py")) + assertThat(EditorConfig.parseSection("*.py}")).isEqualTo(listOf("*.py}")) + } +} @@ -36,16 +36,27 @@ <name>Sonatype Nexus Staging</name> <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url> </repository> + <snapshotRepository> + <id>maven-central</id> + <name>Sonatype Nexus Snapshots</name> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + </snapshotRepository> </distributionManagement> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <kotlin.version>1.1.51</kotlin.version> + <kotlin.version>1.2.41</kotlin.version> + <kotlin.compiler.languageVersion>1.1</kotlin.compiler.languageVersion> + <kotlin.compiler.apiVersion>1.1</kotlin.compiler.apiVersion> <aether.version>1.1.0</aether.version> - <aether.maven.provider.version>3.2.5</aether.maven.provider.version> - <args4j.version>2.33</args4j.version> + <aether.maven.provider.version>3.3.9</aether.maven.provider.version> + <picocli.version>2.3.0</picocli.version> + <kolor.version>0.0.2</kolor.version> <testng.version>6.8.21</testng.version> - <assertj.version>1.7.1</assertj.version> + <assertj.version>3.9.0</assertj.version> + <jimfs.version>1.1</jimfs.version> + <gpg.skip>false</gpg.skip> + <github.description>${project.version}</github.description> </properties> <modules> @@ -62,14 +73,14 @@ <repository> <id>bintray</id> <name>JCenter</name> - <url>http://jcenter.bintray.com</url> + <url>https://jcenter.bintray.com</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>bintray</id> <name>JCenter</name> - <url>http://jcenter.bintray.com</url> + <url>https://jcenter.bintray.com</url> </pluginRepository> </pluginRepositories> @@ -78,7 +89,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> - <version>3.5.1</version> + <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> @@ -126,6 +137,24 @@ </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.0.0-M1</version> + <executions> + <execution> + <id>enforce</id> + <configuration> + <rules> + <dependencyConvergence/> + </rules> + </configuration> + <goals> + <goal>enforce</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> <configuration> @@ -159,7 +188,7 @@ <extension> <groupId>com.github.shyiko.servers-maven-extension</groupId> <artifactId>servers-maven-extension</artifactId> - <version>1.3.0</version> + <version>1.3.1</version> </extension> <extension> <groupId>com.github.shyiko.usage-maven-plugin</groupId> @@ -198,12 +227,12 @@ <plugin> <groupId>org.sonatype.plugins</groupId> <artifactId>nexus-staging-maven-plugin</artifactId> - <version>1.6.7</version> + <version>1.6.8</version> <extensions>true</extensions> <configuration> <nexusUrl>https://oss.sonatype.org/</nexusUrl> <serverId>maven-central</serverId> - <skipStagingRepositoryClose>true</skipStagingRepositoryClose> + <!--<skipStagingRepositoryClose>true</skipStagingRepositoryClose>--> <!--<autoReleaseAfterClose>true</autoReleaseAfterClose>--> </configuration> </plugin> @@ -239,7 +268,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> + <version>1.8</version> <configuration> <target name="update-github-release-notes"> <exec executable="chandler" dir="${basedir}" failonerror="true"> @@ -267,7 +296,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> + <version>1.8</version> <configuration> <target name="deploy-to-homebrew"> <exec executable="${project.basedir}/.deploy-to-homebrew" failonerror="true"> @@ -289,20 +318,24 @@ </property> </activation> <build> - <defaultGoal>antrun:run</defaultGoal> + <defaultGoal>exec:exec@announce</defaultGoal> <plugins> <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-antrun-plugin</artifactId> - <version>1.7</version> - <configuration> - <target name="announce"> - <exec executable="${project.basedir}/.announce" failonerror="true"> - <env key="VERSION" value="${project.version}"/> - <env key="GITHUB_TOKEN" value="${settings.servers.github.privateKey}"/> - </exec> - </target> - </configuration> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.6.0</version> + <executions> + <execution> + <id>announce</id> + <configuration> + <executable>${project.basedir}/.announce</executable> + <environmentVariables> + <VERSION>${project.version}</VERSION> + <GITHUB_TOKEN>${settings.servers.github.privateKey}</GITHUB_TOKEN> + </environmentVariables> + </configuration> + </execution> + </executions> </plugin> </plugins> </build> diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..487626c7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ +include ":ktlint-core" +include ':ktlint-test' +include ':ktlint-ruleset-standard' +include ':ktlint-reporter-plain' +include ':ktlint-reporter-json' +include ':ktlint-reporter-checkstyle' +include ":ktlint" |