FilenameRule does not find lint errors if missing editorconfig (new in 0.40.0)

## Expected Behavior
The `filename` rule should produce a lint error when the kotlin source file name does not match the symbol declared whether or not an editorconfig is present or if it is missing path section which includes the offending file.

## Observed Behavior
Starting in 0.40.0 it does not find this error.
However 0.39.0 does under the same circumstances.

## Steps to Reproduce
Create file `src/main/kotlin/Wrong.kt`
```kotlin
class Right
```
(no editorconfig)
`ktlint src/main/kotlin/Wrong.kt` and it does not find any errors

with editorconfig
```
[*.{kt,kts}]
indent_size = 4
```
a lint error will be found
```
src/main/kotlin/Wrong.kt:1:1: class Right should be declared in a file named Right.kt (cannot be auto-corrected)
```

If you again change the editorconfig file to the below, the lint error will disappear again
```
[src/test/**/*.{kt,kts}]
indent_size = 4
```

## Your Environment
- MacOS
- ktlint 0.40.0
- Java 11

diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
index 0d179a47..9cb236d8 100644
--- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
+++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
@@ -29,6 +29,7 @@ public object KtLint {
     public val EDITOR_CONFIG_USER_DATA_KEY: Key<EditorConfig> = Key<EditorConfig>("EDITOR_CONFIG")
     public val ANDROID_USER_DATA_KEY: Key<Boolean> = Key<Boolean>("ANDROID")
     public val FILE_PATH_USER_DATA_KEY: Key<String> = Key<String>("FILE_PATH")
+    private const val FILE_PATH_PROPERTY = "file_path"
     public val EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY: Key<EditorConfigProperties> =
         Key<EditorConfigProperties>("EDITOR_CONFIG_PROPERTIES")
     public val DISABLED_RULES: Key<Set<String>> = Key<Set<String>>("DISABLED_RULES")
@@ -145,10 +146,15 @@ public object KtLint {
         )

         // Passed-in userData overrides .editorconfig
-        val mergedUserData = editorConfigProperties.convertToRawValues(
-            params.normalizedFilePath,
-            params.isStdIn
-        ) + params.userData
+        val mergedUserData = editorConfigProperties
+            .convertToRawValues() + params.userData
+            .run {
+                if (!params.isStdIn) {
+                    plus(FILE_PATH_PROPERTY to params.normalizedFilePath.toString())
+                } else {
+                    this
+                }
+            }

         injectUserData(rootNode, editorConfigProperties, mergedUserData)

@@ -188,7 +194,7 @@ public object KtLint {
             } else {
                 userData
             }
-        node.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"])
+        node.putUserData(FILE_PATH_USER_DATA_KEY, userData[FILE_PATH_PROPERTY])
         node.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(editorConfigMap - "android" - "file_path"))
         node.putUserData(EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, editorConfigProperties)
         node.putUserData(ANDROID_USER_DATA_KEY, android)
diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt
index 82f5a59c..ab6ebb71 100644
--- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt
+++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt
@@ -121,7 +121,6 @@ class EditorConfigLoader(
     }

     companion object {
-        internal const val FILE_PATH_PROPERTY = "file_path"
         /**
          * List of file extensions, editorconfig lookup will be performed.
          */
@@ -131,31 +130,17 @@ class EditorConfigLoader(
         )

         /**
-         * Converts loaded [editorConfigProperties] values to string representation.
+         * Converts loaded [EditorConfigProperties] values into string representation.
          *
-         * @param filePath added under [FILE_PATH_PROPERTY] key
-         * @param isStdIn indicate that input is from std-in and [filePath] should not be added to the map
-         *
-         * @return converted [editorConfigProperties]
+         * @return map of key as string and value as string property representation
          */
-        fun EditorConfigProperties.convertToRawValues(
-            filePath: Path?,
-            isStdIn: Boolean = false
-        ): Map<String, String> {
+        fun EditorConfigProperties.convertToRawValues(): Map<String, String> {
             return if (isEmpty()) {
                 emptyMap()
             } else {
-                this
-                    .mapValues {
-                        if (it.value.isUnset) "unset" else it.value.sourceValue
-                    }
-                    .run {
-                        if (!isStdIn) {
-                            plus(FILE_PATH_PROPERTY to filePath.toString())
-                        } else {
-                            this
-                        }
-                    }
+                mapValues {
+                    if (it.value.isUnset) "unset" else it.value.sourceValue
+                }
             }
         }
     }
diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt
index 9808342d..9c8cf569 100644
--- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt
+++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt
@@ -74,7 +74,7 @@ internal class EditorConfigLoaderTest {

             val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt")
             val editorConfig = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules)
-            val parsedEditorConfig = editorConfig.convertToRawValues(lintFile)
+            val parsedEditorConfig = editorConfig.convertToRawValues()

             assertThat(parsedEditorConfig).isNotEmpty
             assertThat(parsedEditorConfig)
@@ -86,7 +86,6 @@ internal class EditorConfigLoaderTest {
                     mapOf(
                         "indent_size" to "2",
                         "tab_width" to "2",
-                        EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString()
                     )
                 )
         }
@@ -130,38 +129,35 @@ internal class EditorConfigLoaderTest {

         val lintFileSubdirectory = tempFileSystem.normalizedPath(project1Subdirectory).resolve("test.kt")
         var editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileSubdirectory, rules = rules)
-        var parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFileSubdirectory)
+        var parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "indent_size" to "2",
                 "tab_width" to "2",
                 "indent_style" to "space",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFileSubdirectory.toString()
             )
         )

         val lintFileMainDir = tempFileSystem.normalizedPath(project1Dir).resolve("test.kts")
         editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileMainDir, rules = rules)
-        parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFileMainDir)
+        parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "indent_size" to "4",
                 "tab_width" to "4",
                 "indent_style" to "space",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFileMainDir.toString()
             )
         )

         val lintFileRoot = tempFileSystem.normalizedPath(rootDir).resolve("test.kt")
         editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileRoot, rules = rules)
-        parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFileRoot)
+        parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "end_of_line" to "lf",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFileRoot.toString()
             )
         )
     }
@@ -179,14 +175,13 @@ internal class EditorConfigLoaderTest {

         val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt")
         val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules)
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "insert_final_newline" to "true",
                 "disabled_rules" to "import-ordering",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString()
             )
         )
     }
@@ -203,14 +198,13 @@ internal class EditorConfigLoaderTest {

         val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt")
         val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules)
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "indent_size" to "unset",
                 "tab_width" to "unset",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString()
             )
         )
     }
@@ -227,13 +221,12 @@ internal class EditorConfigLoaderTest {
         val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kts")

         val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules)
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "disabled_rules" to "import-ordering, no-wildcard-imports",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString()
             )
         )
     }
@@ -251,7 +244,7 @@ internal class EditorConfigLoaderTest {
         val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.txt")

         val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules)
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isEmpty()
     }
@@ -272,13 +265,9 @@ internal class EditorConfigLoaderTest {
             rules = rules,
             debug = true,
         )
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(
-            null,
-            true
-        )
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
-        assertThat(parsedEditorConfig).doesNotContainKey(EditorConfigLoader.FILE_PATH_PROPERTY)
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "insert_final_newline" to "true",
@@ -329,13 +318,12 @@ internal class EditorConfigLoaderTest {
             alternativeEditorConfig = tempFileSystem.normalizedPath(anotherDir).resolve(".editorconfig"),
             rules = rules
         )
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "end_of_line" to "lf",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString(),
                 "indent_size" to "2",
                 "tab_width" to "2"
             )
@@ -372,7 +360,7 @@ internal class EditorConfigLoaderTest {
             isStdIn = true,
             rules = rules
         )
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(null, true)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
@@ -402,14 +390,13 @@ internal class EditorConfigLoaderTest {
         Files.createDirectories(lintFile)

         val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, debug = true, rules = rules)
-        val parsedEditorConfig = editorConfigProperties.convertToRawValues(lintFile)
+        val parsedEditorConfig = editorConfigProperties.convertToRawValues()

         assertThat(parsedEditorConfig).isNotEmpty
         assertThat(parsedEditorConfig).isEqualTo(
             mapOf(
                 "insert_final_newline" to "true",
                 "disabled_rules" to "class-must-be-internal",
-                EditorConfigLoader.FILE_PATH_PROPERTY to lintFile.toString()
             )
         )
     }
@@ -421,6 +408,8 @@ internal class EditorConfigLoaderTest {
             node: ASTNode,
             autoCorrect: Boolean,
             emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
-        ) = TODO("Not yet implemented")
+        ) {
+            throw NotImplementedError()
+        }
     }
 }
diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt
index 9372cc1a..c5281681 100644
--- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt
+++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt
@@ -25,6 +25,7 @@ class FilenameRuleTest {
         ) {
             assertThat(
                 FilenameRule().lint(
+                    "/some/path/A.kt",
                     """
                     /*
                      * license
@@ -35,7 +36,6 @@ class FilenameRuleTest {
                     $src
                     //
                     """.trimIndent(),
-                    fileName("/some/path/A.kt")
                 )
             ).isEmpty()
         }
@@ -56,6 +56,7 @@ class FilenameRuleTest {
         ) {
             assertThat(
                 FilenameRule().lint(
+                    "/some/path/B.kt",
                     """
                     /*
                      * license
@@ -66,7 +67,6 @@ class FilenameRuleTest {
                     ${src.key}
                     //
                     """.trimIndent(),
-                    fileName("/some/path/B.kt")
                 )
             ).isEqualTo(
                 listOf(
@@ -80,12 +80,12 @@ class FilenameRuleTest {
     fun testFileWithoutTopLevelDeclarations() {
         assertThat(
             FilenameRule().lint(
+                "A.kt",
                 """
                 /*
                  * copyright
                  */
                 """.trimIndent(),
-                fileName("A.kt")
             )
         ).isEmpty()
     }
@@ -94,11 +94,11 @@ class FilenameRuleTest {
     fun testMultipleTopLevelClasses() {
         assertThat(
             FilenameRule().lint(
+                "A.kt",
                 """
                 class B
                 class C
                 """.trimIndent(),
-                fileName("A.kt")
             )
         ).isEmpty()
     }
@@ -107,13 +107,13 @@ class FilenameRuleTest {
     fun testMultipleNonTopLevelClasses() {
         assertThat(
             FilenameRule().lint(
+                "A.kt",
                 """
                 class B {
                     class C
                     class D
                 }
                 """.trimIndent(),
-                fileName("A.kt")
             )
         ).isEqualTo(
             listOf(
@@ -126,10 +126,10 @@ class FilenameRuleTest {
     fun testCaseSensitiveMatching() {
         assertThat(
             FilenameRule().lint(
+                "woohoo.kt",
                 """
                 interface Woohoo
                 """.trimIndent(),
-                fileName("woohoo.kt")
             )
         ).isEqualTo(
             listOf(
@@ -142,10 +142,10 @@ class FilenameRuleTest {
     fun testCaseEscapedClassNames() {
         assertThat(
             FilenameRule().lint(
+                "B.kt",
                 """
                 class `A`
                 """.trimIndent(),
-                fileName("B.kt")
             )
         ).isEqualTo(
             listOf(
@@ -158,13 +158,11 @@ class FilenameRuleTest {
     fun testIgnoreKotlinScriptFiles() {
         assertThat(
             FilenameRule().lint(
+                "A.kts",
                 """
                 class B
                 """.trimIndent(),
-                fileName("A.kts")
             )
         ).isEmpty()
     }
-
-    private fun fileName(fileName: String) = mapOf("file_path" to fileName)
 }
diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/IntellijIDEAIntegration.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/IntellijIDEAIntegration.kt
index f2e42936..3c28271a 100644
--- a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/IntellijIDEAIntegration.kt
+++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/IntellijIDEAIntegration.kt
@@ -31,10 +31,7 @@ object IntellijIDEAIntegration {
         }
         val editorConfigProperties = EditorConfigLoader(FileSystems.getDefault())
             .loadPropertiesForFile(null, isStdIn = true, rules = emptySet())
-        val editorConfig: Map<String, String> = editorConfigProperties.convertToRawValues(
-            null,
-            isStdIn = true
-        )
+        val editorConfig: Map<String, String> = editorConfigProperties.convertToRawValues()
         val indentSize = editorConfig["indent_size"]?.toIntOrNull() ?: 4
         val continuationIndentSize = editorConfig["continuation_indent_size"]?.toIntOrNull() ?: 4
         val updates = if (local) {

FilenameRule does not find lint errors if missing editorconfig (new in 0.40.0)
## Expected Behavior
The `filename` rule should produce a lint error when the kotlin source file name does not match the symbol declared whether or not an editorconfig is present or if it is missing path section which includes the offending file.

## Observed Behavior
Starting in 0.40.0 it does not find this error.
However 0.39.0 does under the same circumstances.

## Steps to Reproduce
Create file `src/main/kotlin/Wrong.kt`
```kotlin
class Right
```
(no editorconfig)
`ktlint src/main/kotlin/Wrong.kt` and it does not find any errors

with editorconfig
```
[*.{kt,kts}]
indent_size = 4
```
a lint error will be found
```
src/main/kotlin/Wrong.kt:1:1: class Right should be declared in a file named Right.kt (cannot be auto-corrected)
```

If you again change the editorconfig file to the below, the lint error will disappear again
```
[src/test/**/*.{kt,kts}]
indent_size = 4
```

## Your Environment
- MacOS
- ktlint 0.40.0
- Java 11

package yourpkgname

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {

    override fun get(): RuleSet = RuleSet("custom", NoVarRule())
}

@file:JvmName("Main")

package com.pinterest.ktlint

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.ParseException
import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import com.pinterest.ktlint.core.RuleExecutionException
import com.pinterest.ktlint.core.RuleSetProvider
import com.pinterest.ktlint.internal.ApplyToIDEAGloballySubCommand
import com.pinterest.ktlint.internal.ApplyToIDEAProjectSubCommand
import com.pinterest.ktlint.internal.GenerateEditorConfigSubCommand
import com.pinterest.ktlint.internal.GitPreCommitHookSubCommand
import com.pinterest.ktlint.internal.GitPrePushHookSubCommand
import com.pinterest.ktlint.internal.JarFiles
import com.pinterest.ktlint.internal.KtlintVersionProvider
import com.pinterest.ktlint.internal.PrintASTSubCommand
import com.pinterest.ktlint.internal.containsLintError
import com.pinterest.ktlint.internal.fileSequence
import com.pinterest.ktlint.internal.formatFile
import com.pinterest.ktlint.internal.lintFile
import com.pinterest.ktlint.internal.loadBaseline
import com.pinterest.ktlint.internal.loadRulesets
import com.pinterest.ktlint.internal.location
import com.pinterest.ktlint.internal.printHelpOrVersionUsage
import com.pinterest.ktlint.internal.relativeRoute
import com.pinterest.ktlint.internal.toFilesURIList
import com.pinterest.ktlint.reporter.plain.internal.Color
import java.io.File
import java.io.IOException
import java.io.PrintStream
import java.net.URLClassLoader
import java.net.URLDecoder
import java.util.ArrayList
import java.util.LinkedHashMap
import java.util.ServiceLoader
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
import kotlin.system.exitProcess
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters

fun main(args: Array<String>) {
    val ktlintCommand = KtlintCommandLine()
    val commandLine = CommandLine(ktlintCommand)
        .addSubcommand(GitPreCommitHookSubCommand.COMMAND_NAME, GitPreCommitHookSubCommand())
        .addSubcommand(GitPrePushHookSubCommand.COMMAND_NAME, GitPrePushHookSubCommand())
        .addSubcommand(PrintASTSubCommand.COMMAND_NAME, PrintASTSubCommand())
        .addSubcommand(ApplyToIDEAGloballySubCommand.COMMAND_NAME, ApplyToIDEAGloballySubCommand())
        .addSubcommand(ApplyToIDEAProjectSubCommand.COMMAND_NAME, ApplyToIDEAProjectSubCommand())
        .addSubcommand(GenerateEditorConfigSubCommand.COMMAND_NAME, GenerateEditorConfigSubCommand())
    val parseResult = commandLine.parseArgs(*args)

    commandLine.printHelpOrVersionUsage()

    if (parseResult.hasSubcommand()) {
        handleSubCommand(commandLine, parseResult)
    } else {
        ktlintCommand.run()
    }
}

fun handleSubCommand(
    commandLine: CommandLine,
    parseResult: CommandLine.ParseResult
) {
    when (val subCommand = parseResult.subcommand().commandSpec().userObject()) {
        is GitPreCommitHookSubCommand -> subCommand.run()
        is GitPrePushHookSubCommand -> subCommand.run()
        is PrintASTSubCommand -> subCommand.run()
        is ApplyToIDEAGloballySubCommand -> subCommand.run()
        is ApplyToIDEAProjectSubCommand -> subCommand.run()
        is GenerateEditorConfigSubCommand -> subCommand.run()
        else -> commandLine.usage(System.out, CommandLine.Help.Ansi.OFF)
    }
}

@Command(
    headerHeading =
"""An anti-bikeshedding Kotlin linter with built-in formatter
(https://github.com/pinterest/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 uses .gitignore pattern style syntax)
  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=csv,artifact=com.github.user:repo:master-SNAPSHOT

Flags:""",
    synopsisHeading = "",
    customSynopsis = [""],
    sortOptions = false,
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class KtlintCommandLine {

    @Option(
        names = ["--android", "-a"],
        description = ["Turn on Android Kotlin Style Guide compatibility"]
    )
    var android: Boolean = false

    @Option(
        names = ["--color"],
        description = ["Make output colorful"]
    )
    var color: Boolean = false

    @Option(
        names = ["--color-name"],
        description = ["Customize the output color"]
    )
    var colorName: String = Color.DARK_GRAY.name

    @Option(
        names = ["--debug"],
        description = ["Turn on debug output"]
    )
    var debug: Boolean = false

    @Option(
        names = ["--disabled_rules"],
        description = [
            "Comma-separated list of rules to globally disable." +
                " To disable standard ktlint rule-set use --disabled_rules=standard"
        ]
    )
    var disabledRules: String = ""

    // todo: this should have been a command, not a flag (consider changing in 1.0.0)
    @Option(
        names = ["--format", "-F"],
        description = ["Fix any deviations from the code style"]
    )
    private var format: Boolean = false

    @Option(
        names = ["--limit"],
        description = ["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 = ["--relative"],
        description = [
            "Print files relative to the working directory " +
                "(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)"
        ]
    )
    var relative: Boolean = false

    @Option(
        names = ["--reporter"],
        description = [
            "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle, html). " +
                "To use a third-party reporter specify a path to a JAR file on the filesystem."
        ]
    )
    private var reporters: JarFiles = ArrayList<String>()

    @Option(
        names = ["--ruleset", "-R"],
        description = ["A path to a JAR file containing additional ruleset(s)"]
    )
    var rulesets: JarFiles = ArrayList<String>()

    @Option(
        names = ["--stdin"],
        description = ["Read file from stdin"]
    )
    private var stdin: Boolean = false

    @Option(
        names = ["--verbose", "-v"],
        description = ["Show error codes"]
    )
    private var verbose: Boolean = false

    @Option(
        names = ["--editorconfig"],
        description = ["Path to .editorconfig"]
    )
    private var editorConfigPath: String? = null

    @Option(
        names = ["--experimental"],
        description = ["Enabled experimental rules (ktlint-ruleset-experimental)"]
    )
    var experimental: Boolean = false

    @Option(
        names = ["--baseline"],
        description = ["Defines a baseline file to check against"]
    )
    private var baseline: String = ""

    @Parameters(hidden = true)
    private var patterns = ArrayList<String>()

    private val tripped = AtomicBoolean()
    private val fileNumber = AtomicInteger()
    private val errorNumber = AtomicInteger()

    fun run() {
        failOnOldRulesetProviderUsage()

        val start = System.currentTimeMillis()

        val baselineResults = loadBaseline(baseline)
        val ruleSetProviders = rulesets.loadRulesets(experimental, debug, disabledRules)
        var reporter = loadReporter()
        if (baselineResults.baselineGenerationNeeded) {
            val baselineReporter = ReporterTemplate("baseline", null, emptyMap(), baseline)
            val reporterProviderById = loadReporters(emptyList())
            reporter = Reporter.from(reporter, baselineReporter.toReporter(reporterProviderById))
        }
        val userData = listOfNotNull(
            "android" to android.toString(),
            if (disabledRules.isNotBlank()) "disabled_rules" to disabledRules else null
        ).toMap()

        reporter.beforeAll()
        if (stdin) {
            lintStdin(ruleSetProviders, userData, reporter)
        } else {
            lintFiles(ruleSetProviders, userData, baselineResults.baselineRules, reporter)
        }
        reporter.afterAll()
        if (debug) {
            System.err.println(
                "[DEBUG] ${
                System.currentTimeMillis() - start
                }ms / $fileNumber file(s) / $errorNumber error(s)"
            )
        }
        if (tripped.get()) {
            exitProcess(1)
        }
    }

    private fun lintFiles(
        ruleSetProviders: Map<String, RuleSetProvider>,
        userData: Map<String, String>,
        baseline: Map<String, List<LintError>>?,
        reporter: Reporter
    ) {
        patterns.fileSequence()
            .takeWhile { errorNumber.get() < limit }
            .map { file ->
                Callable {
                    file to process(
                        file.path,
                        file.readText(),
                        ruleSetProviders,
                        userData,
                        baseline?.get(file.relativeRoute)
                    )
                }
            }
            .parallel({ (file, errList) -> report(file.location(relative), errList, reporter) })
    }

    private fun lintStdin(
        ruleSetProviders: Map<String, RuleSetProvider>,
        userData: Map<String, String>,
        reporter: Reporter
    ) {
        report(
            KtLint.STDIN_FILE,
            process(
                KtLint.STDIN_FILE,
                String(System.`in`.readBytes()),
                ruleSetProviders,
                userData,
                null
            ),
            reporter
        )
    }

    /**
     * Detect custom rulesets that have not been moved to the new package.
     */
    @Suppress("Deprecation")
    private fun failOnOldRulesetProviderUsage() {
        if (ServiceLoader.load(com.github.shyiko.ktlint.core.RuleSetProvider::class.java).any()) {
            System.err.println("[ERROR] Cannot load custom ruleset!")
            System.err.println("[ERROR] RuleSetProvider has moved to com.pinterest.ktlint.core.")
            System.err.println("[ERROR] Please rename META-INF/services/com.github.shyiko.ktlint.core.RuleSetProvider to META-INF/services/com.pinterest.ktlint.core.RuleSetProvider")
            exitProcess(1)
        }
    }

    private fun report(
        fileName: String,
        errList: List<LintErrorWithCorrectionInfo>,
        reporter: Reporter
    ) {
        fileNumber.incrementAndGet()
        val errListLimit = minOf(errList.size, maxOf(limit - errorNumber.get(), 0))
        errorNumber.addAndGet(errListLimit)

        reporter.before(fileName)
        errList.head(errListLimit).forEach { (err, corrected) ->
            reporter.onLintError(
                fileName,
                if (!err.canBeAutoCorrected) err.copy(detail = err.detail + " (cannot be auto-corrected)") else err,
                corrected
            )
        }
        reporter.after(fileName)
    }

    private fun process(
        fileName: String,
        fileContent: String,
        ruleSetProviders: Map<String, RuleSetProvider>,
        userData: Map<String, String>,
        baselineErrors: List<LintError>?
    ): List<LintErrorWithCorrectionInfo> {
        if (debug) {
            val fileLocation = if (fileName != KtLint.STDIN_FILE) File(fileName).location(relative) else fileName
            System.err.println("[DEBUG] Checking $fileLocation")
        }
        val result = ArrayList<LintErrorWithCorrectionInfo>()
        if (format) {
            val formattedFileContent = try {
                formatFile(
                    fileName,
                    fileContent,
                    ruleSetProviders.map { it.value.get() },
                    userData,
                    editorConfigPath,
                    debug
                ) { err, corrected ->
                    if (!corrected) {
                        if (baselineErrors == null || !baselineErrors.containsLintError(err)) {
                            result.add(LintErrorWithCorrectionInfo(err, corrected))
                            tripped.set(true)
                        }
                    }
                }
            } catch (e: Exception) {
                result.add(LintErrorWithCorrectionInfo(e.toLintError(), false))
                tripped.set(true)
                fileContent // making sure `cat file | ktlint --stdint > file` is (relatively) safe
            }
            if (stdin) {
                print(formattedFileContent)
            } else {
                if (fileContent !== formattedFileContent) {
                    File(fileName).writeText(formattedFileContent, charset("UTF-8"))
                }
            }
        } else {
            try {
                lintFile(
                    fileName,
                    fileContent,
                    ruleSetProviders.map { it.value.get() },
                    userData,
                    editorConfigPath,
                    debug
                ) { err ->
                    if (baselineErrors == null || !baselineErrors.containsLintError(err)) {
                        result.add(LintErrorWithCorrectionInfo(err, false))
                        tripped.set(true)
                    }
                }
            } catch (e: Exception) {
                result.add(LintErrorWithCorrectionInfo(e.toLintError(), false))
                tripped.set(true)
            }
        }
        return result
    }

    private fun loadReporter(): Reporter {
        val configuredReporters = if (reporters.isEmpty()) listOf("plain") else reporters

        val tpls = configuredReporters
            .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(), "color_name" to colorName) + parseQuery(rawReporterConfig),
                    split.lastOrNull { it.startsWith("output=") }?.let { it.split("=")[1] }
                )
            }
            .distinct()
        val reporterProviderById = loadReporters(tpls.mapNotNull { it.artifact })
        return Reporter.from(*tpls.map { it.toReporter(reporterProviderById) }.toTypedArray())
    }

    private fun ReporterTemplate.toReporter(
        reporterProviderById: Map<String, ReporterProvider>
    ): 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()
                            if (tripped.get()) {
                                val outputLocation = File(output).absoluteFile.location(relative)
                                System.err.println("\"$id\" report written to $outputLocation")
                            }
                        }
                    }
                } else {
                    reporter
                }
            }
    }

    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/pinterest/ktlint/issues " +
                        "(if possible, provide the source code that triggered an error)"
                )
            }
            else -> throw e
        }
    }

    private fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit)

    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],
                            URLDecoder.decode(e.getOrElse(1) { "true" }, "UTF-8")
                        )
                    }
                }
                map
            }

    private fun File.mkdirsOrFail() {
        if (!mkdirs() && !isDirectory) {
            throw IOException("Unable to create \"${this}\" directory")
        }
    }

    /**
     * Executes "Callable"s in parallel (lazily).
     * The results are gathered one-by-one (by `cb(<callable result>)`) in the order of corresponding "Callable"s
     * in the "Sequence" (think `seq.toList().map { executorService.submit(it) }.forEach { cb(it.get()) }` but without
     * buffering an entire sequence).
     *
     * Once kotlinx-coroutines are out of "experimental" stage everything below can be replaced with
     * ```
     * suspend fun <T> Sequence<Callable<T>>.parallel(...) {
     *     val ctx = newFixedThreadPoolContext(numberOfThreads, "Sequence<Callable<T>>.parallel")
     *     ctx.use {
     *         val channel = produce(ctx, numberOfThreads) {
     *             for (task in this@parallel) {
     *                 send(async(ctx) { task.call() })
     *             }
     *         }
     *         for (res in channel) {
     *             cb(res.await())
     *         }
     *     }
     * }
     * ```
     */
    private fun <T> Sequence<Callable<T>>.parallel(
        cb: (T) -> Unit,
        numberOfThreads: Int = Runtime.getRuntime().availableProcessors()
    ) {
        val pill = object : Future<T> {
            override fun isDone(): Boolean { throw UnsupportedOperationException() }
            override fun get(timeout: Long, unit: TimeUnit): T { throw UnsupportedOperationException() }
            override fun get(): T { throw UnsupportedOperationException() }
            override fun cancel(mayInterruptIfRunning: Boolean): Boolean { throw UnsupportedOperationException() }
            override fun isCancelled(): Boolean { throw UnsupportedOperationException() }
        }
        val q = ArrayBlockingQueue<Future<T>>(numberOfThreads)
        val producer = thread(start = true) {
            val executorService = Executors.newCachedThreadPool()
            try {
                for (task in this) {
                    q.put(executorService.submit(task))
                }
                q.put(pill)
            } catch (e: InterruptedException) {
                // we've been asked to stop consuming sequence
            } finally {
                executorService.shutdown()
            }
        }
        try {
            while (true) {
                val result = q.take()
                if (result != pill) cb(result.get()) else break
            }
        } finally {
            producer.interrupt() // in case q.take()/result.get() throws
            producer.join()
        }
    }

    private fun loadReporters(externalReportersJarPaths: List<String>) = ServiceLoader
        .load(
            ReporterProvider::class.java,
            URLClassLoader(externalReportersJarPaths.toFilesURIList().toTypedArray())
        )
        .associateBy { it.id }
        .also {
            if (debug) {
                it.forEach { entry ->
                    println("[DEBUG] Discovered reporter with \"${entry.key}\" id.")
                }
            }
        }

    private data class LintErrorWithCorrectionInfo(
        val err: LintError,
        val corrected: Boolean
    )

    private data class ReporterTemplate(
        val id: String,
        val artifact: String?,
        val config: Map<String, String>,
        val output: String?
    )
}

package com.pinterest.ktlint.core

/**
 * @see [EditorConfig](http://editorconfig.org/)
 *
 * This class is injected into the user data, so it is available to rules via [KtLint.EDITOR_CONFIG_USER_DATA_KEY]
 */
interface EditorConfig {

    enum class IndentStyle { SPACE, TAB }

    val indentStyle: IndentStyle
    val indentSize: Int
    val tabWidth: Int
    val maxLineLength: Int
    @Deprecated(
        message = "Not used anymore by rules, please use 'insert_final_newline' directly via get()"
    )
    val insertFinalNewline: Boolean

    operator fun get(key: String): String?

    companion object {
        fun fromMap(map: Map<String, String>): EditorConfig {
            val indentStyle = when {
                map["indent_style"]?.toLowerCase() == "tab" -> IndentStyle.TAB
                else -> IndentStyle.SPACE
            }
            val indentSize = map["indent_size"].let { v ->
                if (v?.toLowerCase() == "unset") -1 else v?.toIntOrNull() ?: 4
            }
            val tabWidth = map["indent_size"]?.toIntOrNull()
            val maxLineLength = map["max_line_length"]?.toIntOrNull() ?: -1
            val insertFinalNewline = map["insert_final_newline"]?.toBoolean() ?: true
            return object : EditorConfig {
                override val indentStyle = indentStyle
                override val indentSize = indentSize
                override val tabWidth = tabWidth ?: indentSize
                override val maxLineLength = maxLineLength
                override val insertFinalNewline = insertFinalNewline
                override fun get(key: String): String? = map[key]
            }
        }
    }
}

package com.pinterest.ktlint.core

import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.ast.visit
import com.pinterest.ktlint.core.internal.EditorConfigGenerator
import com.pinterest.ktlint.core.internal.EditorConfigLoader
import com.pinterest.ktlint.core.internal.EditorConfigLoader.Companion.convertToRawValues
import com.pinterest.ktlint.core.internal.LineAndColumn
import com.pinterest.ktlint.core.internal.SuppressionLocator
import com.pinterest.ktlint.core.internal.buildPositionInTextLocator
import com.pinterest.ktlint.core.internal.buildSuppressedRegionsLocator
import com.pinterest.ktlint.core.internal.initPsiFileFactory
import com.pinterest.ktlint.core.internal.noSuppression
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.lang.FileASTNode
import org.jetbrains.kotlin.com.intellij.openapi.util.Key
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiErrorElement
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.psi.KtFile

public object KtLint {

    public val EDITOR_CONFIG_USER_DATA_KEY: Key<EditorConfig> = Key<EditorConfig>("EDITOR_CONFIG")
    public val ANDROID_USER_DATA_KEY: Key<Boolean> = Key<Boolean>("ANDROID")
    public val FILE_PATH_USER_DATA_KEY: Key<String> = Key<String>("FILE_PATH")
    public val EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY: Key<EditorConfigProperties> =
        Key<EditorConfigProperties>("EDITOR_CONFIG_PROPERTIES")
    public val DISABLED_RULES: Key<Set<String>> = Key<Set<String>>("DISABLED_RULES")
    private const val UTF8_BOM = "\uFEFF"
    public const val STDIN_FILE: String = "<stdin>"

    private val psiFileFactory: PsiFileFactory = initPsiFileFactory()
    private val editorConfigLoader = EditorConfigLoader(FileSystems.getDefault())

    @OptIn(FeatureInAlphaState::class)
    private val editorConfigGenerator = EditorConfigGenerator(editorConfigLoader)

    /**
     * @param fileName path of file to lint/format
     * @param text Contents of file to lint/format
     * @param ruleSets a collection of "RuleSet"s used to validate source
     * @param userData Map of user options
     * @param cb callback invoked for each lint error
     * @param script true if this is a Kotlin script file
     * @param editorConfigPath optional path of the .editorconfig file (otherwise will use working directory)
     * @param debug True if invoked with the --debug flag
     */
    public data class Params(
        val fileName: String? = null,
        val text: String,
        val ruleSets: Iterable<RuleSet>,
        val userData: Map<String, String> = emptyMap(),
        val cb: (e: LintError, corrected: Boolean) -> Unit,
        val script: Boolean = false,
        val editorConfigPath: String? = null,
        val debug: Boolean = false
    ) {
        internal val normalizedFilePath: Path? get() = if (fileName == STDIN_FILE || fileName == null) {
            null
        } else {
            Paths.get(fileName)
        }

        internal val isStdIn: Boolean get() = fileName == STDIN_FILE

        internal val rules: Set<Rule> get() = ruleSets
            .flatMap {
                it.rules.toList()
            }
            .toSet()
    }

    /**
     * Check source for lint errors.
     *
     * @throws ParseException if text is not a valid Kotlin code
     * @throws RuleExecutionException in case of internal failure caused by a bug in rule implementation
     */
    public fun lint(params: Params) {
        val preparedCode = prepareCodeForLinting(params)
        val errors = mutableListOf<LintError>()

        visitor(preparedCode.rootNode, params.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 (
                !preparedCode.suppressedRegionLocator(node.startOffset, fqRuleId, node === preparedCode.rootNode)
            ) {
                try {
                    rule.visit(node, false) { offset, errorMessage, canBeAutoCorrected ->
                        // https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189
                        if (node.startOffset != offset &&
                            preparedCode.suppressedRegionLocator(offset, fqRuleId, node === preparedCode.rootNode)
                        ) {
                            return@visit
                        }
                        val (line, col) = preparedCode.positionInTextLocator(offset)
                        errors.add(LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected))
                    }
                } catch (e: Exception) {
                    val (line, col) = preparedCode.positionInTextLocator(node.startOffset)
                    throw RuleExecutionException(line, col, fqRuleId, e)
                }
            }
        }

        errors
            .sortedWith { l, r -> if (l.line != r.line) l.line - r.line else l.col - r.col }
            .forEach { e -> params.cb(e, false) }
    }

    private fun prepareCodeForLinting(
        params: Params
    ): PreparedCode {
        val normalizedText = normalizeText(params.text)
        val positionInTextLocator = buildPositionInTextLocator(normalizedText)

        val psiFileName = if (params.script) "file.kts" else "file.kt"
        val psiFile = psiFileFactory.createFileFromText(
            psiFileName,
            KotlinLanguage.INSTANCE,
            normalizedText
        ) as KtFile

        val errorElement = psiFile.findErrorElement()
        if (errorElement != null) {
            val (line, col) = positionInTextLocator(errorElement.textOffset)
            throw ParseException(line, col, errorElement.errorDescription)
        }

        val rootNode = psiFile.node

        val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(
            params.normalizedFilePath,
            params.isStdIn,
            params.editorConfigPath?.let { Paths.get(it) },
            params.rules,
            params.debug
        )

        // Passed-in userData overrides .editorconfig
        val mergedUserData = editorConfigProperties.convertToRawValues(
            params.normalizedFilePath,
            params.isStdIn
        ) + params.userData

        injectUserData(rootNode, editorConfigProperties, mergedUserData)

        val suppressedRegionLocator = buildSuppressedRegionsLocator(rootNode)

        return PreparedCode(
            rootNode,
            positionInTextLocator,
            suppressedRegionLocator
        )
    }

    @Deprecated(
        message = "Should not be a part of public api. Will be removed in future release.",
        level = DeprecationLevel.WARNING
    )
    public fun normalizeText(text: String): String {
        return text
            .replace("\r\n", "\n")
            .replace("\r", "\n")
            .replaceFirst(UTF8_BOM, "")
    }

    private val Map<String, String>.isAndroidCodeStyle get() = get("android")?.toBoolean() ?: false

    private fun injectUserData(
        node: ASTNode,
        editorConfigProperties: EditorConfigProperties,
        userData: Map<String, String>
    ) {
        val android = userData.isAndroidCodeStyle
        val editorConfigMap =
            if (android &&
                userData["max_line_length"].let { it?.toLowerCase() != "off" && it?.toIntOrNull() == null }
            ) {
                userData + mapOf("max_line_length" to "100")
            } else {
                userData
            }
        node.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"])
        node.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(editorConfigMap - "android" - "file_path"))
        node.putUserData(EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, editorConfigProperties)
        node.putUserData(ANDROID_USER_DATA_KEY, android)
        node.putUserData(
            DISABLED_RULES,
            userData["disabled_rules"]?.split(",")?.map { it.trim() }?.toSet() ?: emptySet()
        )
    }

    private fun visitor(
        rootNode: ASTNode,
        ruleSets: Iterable<RuleSet>,
        concurrent: Boolean = true,
        filter: (rootNode: ASTNode, fqRuleId: String) -> Boolean = this::filterDisabledRules

    ): ((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}:"
            for (rule in ruleSet) {
                val fqRuleId = "$prefix${rule.id}"
                if (!filter(rootNode, 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 filterDisabledRules(rootNode: ASTNode, fqRuleId: String): Boolean {
        return rootNode.getUserData(DISABLED_RULES)?.contains(fqRuleId) == false
    }

    @Deprecated(
        message = "Should not be a part of public api. Will be removed in future release.",
        level = DeprecationLevel.WARNING
    )
    public fun calculateLineColByOffset(
        text: String
    ): (offset: Int) -> Pair<Int, Int> {
        return buildPositionInTextLocator(text)
    }

    /**
     * Fix style violations.
     *
     * @throws ParseException if text is not a valid Kotlin code
     * @throws RuleExecutionException in case of internal failure caused by a bug in rule implementation
     */
    public fun format(params: Params): String {
        val hasUTF8BOM = params.text.startsWith(UTF8_BOM)
        val preparedCode = prepareCodeForLinting(params)

        var tripped = false
        var mutated = false
        visitor(preparedCode.rootNode, params.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 (
                    !preparedCode.suppressedRegionLocator(node.startOffset, fqRuleId, node === preparedCode.rootNode)
                ) {
                    try {
                        rule.visit(node, true) { _, _, canBeAutoCorrected ->
                            tripped = true
                            if (canBeAutoCorrected) {
                                mutated = true
                                if (preparedCode.suppressedRegionLocator !== noSuppression) {
                                    preparedCode.suppressedRegionLocator = buildSuppressedRegionsLocator(
                                        preparedCode.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, fqRuleId, e)
                    }
                }
            }
        if (tripped) {
            val errors = mutableListOf<Pair<LintError, Boolean>>()
            visitor(preparedCode.rootNode, params.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 (
                    !preparedCode.suppressedRegionLocator(node.startOffset, fqRuleId, node === preparedCode.rootNode)
                ) {
                    try {
                        rule.visit(node, false) { offset, errorMessage, canBeAutoCorrected ->
                            // https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189
                            if (
                                node.startOffset != offset &&
                                preparedCode.suppressedRegionLocator(offset, fqRuleId, node === preparedCode.rootNode)
                            ) {
                                return@visit
                            }
                            val (line, col) = preparedCode.positionInTextLocator(offset)
                            errors.add(Pair(LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected), false))
                        }
                    } catch (e: Exception) {
                        val (line, col) = preparedCode.positionInTextLocator(node.startOffset)
                        throw RuleExecutionException(line, col, fqRuleId, e)
                    }
                }
            }

            errors
                .sortedWith { (l), (r) -> if (l.line != r.line) l.line - r.line else l.col - r.col }
                .forEach { (e, corrected) -> params.cb(e, corrected) }
        }

        if (!mutated) {
            return params.text
        }

        return if (hasUTF8BOM) UTF8_BOM else "" + // Restore UTF8 BOM if it was present
            preparedCode
                .rootNode
                .text
                .replace("\n", determineLineSeparator(params.text, params.userData))
    }

    /**
     * Reduce memory usage of all internal caches.
     */
    public fun trimMemory() {
        editorConfigLoader.trimMemory()
    }

    /**
     * Generates Kotlin `.editorconfig` file section content based on [Params.ruleSets].
     *
     * Method loads merged `.editorconfig` content from [Params.fileName] path,
     * and then, by querying rules from [Params.ruleSets] for missing properties default values,
     * generates Kotlin section (default is `[*.{kt,kts}]`) new content.
     *
     * Rule should implement [UsesEditorConfigProperties] interface to support this.
     *
     * @return Kotlin section editorconfig content. For example:
     * ```properties
     * final-newline=true
     * indent-size=4
     * ```
     */
    @FeatureInAlphaState
    public fun generateKotlinEditorConfigSection(
        params: Params
    ): String {
        val filePath = params.normalizedFilePath
        requireNotNull(filePath) {
            "Please pass path to existing Kotlin file"
        }
        return editorConfigGenerator.generateEditorconfig(
            filePath,
            params.rules,
            params.userData.isAndroidCodeStyle,
            params.debug
        )
    }

    private fun determineLineSeparator(fileContent: String, userData: Map<String, String>): String {
        val eol = userData["end_of_line"]?.trim()?.toLowerCase()
        return when {
            eol == "native" -> System.lineSeparator()
            eol == "crlf" || eol != "lf" && fileContent.lastIndexOf('\r') != -1 -> "\r\n"
            else -> "\n"
        }
    }

    private fun PsiElement.findErrorElement(): PsiErrorElement? {
        if (this is PsiErrorElement) {
            return this
        }
        this.children.forEach { child ->
            val errorElement = child.findErrorElement()
            if (errorElement != null) {
                return errorElement
            }
        }
        return null
    }

    private class PreparedCode(
        val rootNode: FileASTNode,
        val positionInTextLocator: (offset: Int) -> LineAndColumn,
        var suppressedRegionLocator: SuppressionLocator
    )
}

package com.pinterest.ktlint.core

/**
 * Lint error.
 *
 * @param line line number (one-based)
 * @param col column number (one-based)
 * @param ruleId rule id (prepended with "<ruleSetId>:" in case of non-standard ruleset)
 * @param detail error message
 */
data class LintError(val line: Int, val col: Int, val ruleId: String, val detail: String) {

    // fixme:
    // not included in equals/hashCode for backward-compatibility with ktlint < 0.25.0
    // subject to change in 1.0.0
    var canBeAutoCorrected: Boolean = false
        private set(value) {
            field = value
        }

    constructor(
        line: Int,
        col: Int,
        ruleId: String,
        detail: String,
        canBeAutoCorrected: Boolean
    ) : this(line, col, ruleId, detail) {
        this.canBeAutoCorrected = canBeAutoCorrected
    }
}

package com.pinterest.ktlint.core

/**
 * @param line line number (one-based)
 * @param col column number (one-based)
 * @param message message
 */
class ParseException(val line: Int, val col: Int, message: String) : RuntimeException("$line:$col $message")

package com.pinterest.ktlint.core

/**
 * Implementation must be thread-safe. In particular, [onLintError] might be called in parallel for
 * different files (but not for the same file) ([before], [onLintError] and [after] are guarantied to be called
 * on the same thread).
 * @see ReporterProvider
 */
interface Reporter {

    /**
     * Called once, before the processing begins
     * (regardless of whether any files matching the pattern are (going to) found).
     * It's guarantied to be called before any of the other [Reporter]s methods.
     */
    fun beforeAll() {}
    /**
     * Called when file (matching the pattern) is found but before it's parsed.
     */
    fun before(file: String) {}
    fun onLintError(file: String, err: LintError, corrected: Boolean)
    /**
     * Called after ktlint is done with the file.
     */
    fun after(file: String) {}
    /**
     * Called once, after all the files (if any) have been processed.
     * It's guarantied to be called after all other [Reporter]s methods.
     */
    fun afterAll() {}

    companion object {
        fun from(vararg reporters: Reporter): Reporter {
            return object : Reporter {
                override fun beforeAll() { reporters.forEach(Reporter::beforeAll) }
                override fun before(file: String) { reporters.forEach { it.before(file) } }
                override fun onLintError(file: String, err: LintError, corrected: Boolean) =
                    reporters.forEach { it.onLintError(file, err, corrected) }
                override fun after(file: String) { reporters.forEach { it.after(file) } }
                override fun afterAll() { reporters.forEach(Reporter::afterAll) }
            }
        }
    }
}

package com.pinterest.ktlint.core

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
 * `META-INF/services/com.pinterest.ktlint.core.ReporterProvider`
 * (see `ktlint-reporter-plain/src/main/resources` for an example).
 */
interface ReporterProvider {
    val id: String
    fun get(out: PrintStream, opt: Map<String, String>): Reporter
}

package com.pinterest.ktlint.core

import com.pinterest.ktlint.core.internal.IdNamingPolicy
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).
 *
 * @param id must be unique within the ruleset
 *
 * @see RuleSet
 */
abstract class Rule(val id: String) {

    init {
        IdNamingPolicy.enforceNaming(id)
    }

    /**
     * This method is going to be executed for each node in AST (in DFS fashion).
     *
     * @param node AST node
     * @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
    )

    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
    }
}

package com.pinterest.ktlint.core

/**
 * @param line line number (one-based)
 * @param col column number (one-based)
 * @param ruleId rule id (prepended with "<ruleSetId>:" in case of non-standard ruleset)
 */
class RuleExecutionException(val line: Int, val col: Int, val ruleId: String, cause: Throwable) :
    RuntimeException(cause)

package com.pinterest.ktlint.core

import com.pinterest.ktlint.core.internal.IdNamingPolicy

/**
 * A group of [Rule]s discoverable through [RuleSetProvider].
 * @see RuleSetProvider
 */
open class RuleSet(
    val id: String,
    vararg val rules: Rule
) : Iterable<Rule> {

    init {
        IdNamingPolicy.enforceNaming(id)
        require(rules.isNotEmpty()) { "At least one rule must be provided" }
    }

    override fun iterator(): Iterator<Rule> = rules.iterator()
}

package com.pinterest.ktlint.core

/**
 * `ktlint` uses [ServiceLoader](http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html) to
 * discover all available `RuleSetProvider`s on the classpath and so each `RuleSetProvider` must be registered using
 * `META-INF/services/com.pinterest.ktlint.core.RuleSetProvider`
 * (see `ktlint-ruleset-standard/src/main/resources` for an example).
 */
interface RuleSetProvider {

    /**
     * This method is going to be called once for each file (which means if any of the rules have state or
     * are not thread-safe - a new RuleSet must be created).
     */
    fun get(): RuleSet
}

package com.pinterest.ktlint.internal

import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess

class ApplyToIDEACommandHelper(
    private val applyToProject: Boolean,
    private val forceApply: Boolean,
    private val isAndroidCodeStyle: Boolean
) {
    fun apply() {
        try {
            val workDir = Paths.get(".")

            if (!forceApply && !getUserAcceptanceToUpdateFiles(workDir)) {
                println("Update canceled.")
                exitProcess(1)
            }

            IntellijIDEAIntegration.apply(
                workDir,
                false,
                isAndroidCodeStyle,
                applyToProject
            )
        } catch (e: IntellijIDEAIntegration.ProjectNotFoundException) {
            println(".idea directory not found. Are you sure you are inside project root directory?")
            exitProcess(1)
        }

        println(
            """
            |Updated.
            |Please restart your IDE.
            |If you experience any issues please report them at https://github.com/pinterest/ktlint/issues.
            """.trimMargin()
        )
    }

    private fun getUserAcceptanceToUpdateFiles(workDir: Path): Boolean {
        val fileList = IntellijIDEAIntegration.apply(
            workDir,
            true,
            isAndroidCodeStyle,
            applyToProject
        )
        println(
            """
            |The following files are going to be updated:
            |${fileList.joinToString(prefix = "\t", separator = "\n\t")}
            |
            |Do you wish to proceed? [y/n]
            |(in future, use -y flag if you wish to skip confirmation)
            """.trimMargin()
        )

        val userInput = generateSequence { readLine() }
            .filter { it.trim().isNotBlank() }
            .first()

        return "y".equals(userInput, ignoreCase = true)
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "Update Intellij IDEA Kotlin codestyle settings (global)"
    ],
    aliases = ["--apply-to-idea"],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class ApplyToIDEAGloballySubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    @CommandLine.Option(
        names = ["-y"],
        description = ["Overwrite existing Kotlin codestyle settings without asking"]
    )
    private var forceApply: Boolean = false

    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        ApplyToIDEACommandHelper(
            false,
            forceApply,
            ktlintCommand.android
        ).apply()
    }

    companion object {
        const val COMMAND_NAME = "applyToIDEA"
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "Update Intellij IDEA project settings"
    ],
    aliases = ["--apply-to-idea-project"],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class ApplyToIDEAProjectSubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    @CommandLine.Option(
        names = ["-y"],
        description = ["Overwrite existing Kotlin codestyle settings without asking"]
    )
    private var forceApply: Boolean = false

    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        ApplyToIDEACommandHelper(
            true,
            forceApply,
            ktlintCommand.android
        ).apply()
    }

    companion object {
        const val COMMAND_NAME = "applyToIDEAProject"
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.core.LintError
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.nio.file.Paths
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import org.w3c.dom.Element
import org.xml.sax.SAXException

/**
 * Loads the baseline file if one is provided.
 *
 * @param baselineFilePath the path to the xml baseline file
 * @return a [CurrentBaseline] with the file details
 */
internal fun loadBaseline(baselineFilePath: String): CurrentBaseline {
    if (baselineFilePath.isBlank()) {
        return CurrentBaseline(null, false)
    }

    var baselineRules: Map<String, List<LintError>>? = null
    var baselineGenerationNeeded = true
    val baselineFile = Paths.get(baselineFilePath).toFile()
    if (baselineFile.exists()) {
        try {
            baselineRules = parseBaseline(baselineFile.inputStream())
            baselineGenerationNeeded = false
        } catch (e: IOException) {
            System.err.println("Unable to parse baseline file: $baselineFilePath")
            baselineGenerationNeeded = true
        } catch (e: ParserConfigurationException) {
            System.err.println("Unable to parse baseline file: $baselineFilePath")
            baselineGenerationNeeded = true
        } catch (e: SAXException) {
            System.err.println("Unable to parse baseline file: $baselineFilePath")
            baselineGenerationNeeded = true
        }
    }

    // delete the old file if one exists
    if (baselineGenerationNeeded && baselineFile.exists()) {
        baselineFile.delete()
    }

    return CurrentBaseline(baselineRules, baselineGenerationNeeded)
}

/**
 * Parses the file to generate a mapping of [LintError]
 *
 * @param baselineFile the file containing the current baseline
 * @return a mapping of file names to a list of all [LintError] in that file
 */
internal fun parseBaseline(baselineFile: InputStream): Map<String, List<LintError>> {
    val baselineRules = HashMap<String, MutableList<LintError>>()
    val builderFactory = DocumentBuilderFactory.newInstance()
    val docBuilder = builderFactory.newDocumentBuilder()
    val doc = docBuilder.parse(baselineFile)
    val filesList = doc.getElementsByTagName("file")
    for (i in 0 until filesList.length) {
        val fileElement = filesList.item(i) as Element
        val fileName = fileElement.getAttribute("name")
        val baselineErrors = parseBaselineErrorsByFile(fileElement)
        baselineRules[fileName] = baselineErrors
    }
    return baselineRules
}

/**
 * Parses the errors inside each file tag in the xml
 *
 * @param element the xml "file" element
 * @return a list of [LintError] for that file
 */
private fun parseBaselineErrorsByFile(element: Element): MutableList<LintError> {
    val errors = mutableListOf<LintError>()
    val errorsList = element.getElementsByTagName("error")
    for (i in 0 until errorsList.length) {
        val errorElement = errorsList.item(i) as Element
        errors.add(
            LintError(
                line = errorElement.getAttribute("line").toInt(),
                col = errorElement.getAttribute("column").toInt(),
                ruleId = errorElement.getAttribute("source"),
                detail = "" // we don't have details in the baseline file
            )
        )
    }
    return errors
}

internal class CurrentBaseline(
    val baselineRules: Map<String, List<LintError>>?,
    val baselineGenerationNeeded: Boolean
)

/**
 * Checks if the list contains the lint error. We cannot use the contains function
 * as the `checkstyle` reporter formats the details string and hence the comparison
 * normally fails
 */
internal fun List<LintError>.containsLintError(error: LintError): Boolean {
    return firstOrNull { lintError ->
        lintError.col == error.col &&
            lintError.line == error.line &&
            lintError.ruleId == error.ruleId
    } != null
}

/**
 * Gets the relative route of the file for baselines
 * Also adjusts the slashes for uniformity between file systems
 */
internal val File.relativeRoute: String
    get() {
        val rootPath = Paths.get("").toAbsolutePath()
        val filePath = this.toPath()
        return rootPath.relativize(filePath).toString().replace(File.separatorChar, '/')
    }

package com.pinterest.ktlint.internal

import java.math.BigInteger
import java.security.MessageDigest

/**
 * Generate hex string for given [ByteArray] content.
 */
internal val ByteArray.hex get() = BigInteger(MessageDigest.getInstance("SHA-256").digest(this)).toString(16)

package com.pinterest.ktlint.internal

import kotlin.system.exitProcess
import picocli.CommandLine

/**
 * Check if user requested either help or version options, if yes - print it
 * and exit process with [exitCode] exit code.
 */
internal fun CommandLine.printHelpOrVersionUsage(
    exitCode: Int = 0
) {
    if (isUsageHelpRequested) {
        usage(System.out, CommandLine.Help.Ansi.OFF)
        exitProcess(exitCode)
    } else if (isVersionHelpRequested) {
        printVersionHelp(System.out, CommandLine.Help.Ansi.OFF)
        exitProcess(exitCode)
    }
}

package com.pinterest.ktlint.internal

import com.github.shyiko.klob.Glob
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.RuleSet
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess

internal val workDir: String = File(".").canonicalPath

internal fun List<String>.fileSequence(): Sequence<File> {
    val kotlinFiles: Sequence<Path> = if (isEmpty()) {
        Glob.from("**/*.kt", "**/*.kts")
            .iterate(
                Paths.get(workDir),
                Glob.IterationOption.SKIP_HIDDEN
            )
            .asSequence()
    } else {
        // Converting List<String> to Array<String> and passing it to Glob.from(patterns) skips some files
        // See https://github.com/pinterest/ktlint/issues/942
        map {
            Glob.from(expandTilde(it))
                .iterate(Paths.get(workDir))
        }
            .asSequence()
            .flatMap { it.asSequence() }
    }

    return kotlinFiles
        .map(Path::toFile)
}

/**
 * List of paths to Java `jar` files.
 */
internal typealias JarFiles = List<String>

internal fun JarFiles.toFilesURIList() = map {
    val jarFile = File(expandTilde(it))
    if (!jarFile.exists()) {
        println("Error: $it does not exist")
        exitProcess(1)
    }
    jarFile.toURI().toURL()
}

// 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 (~/)
private fun expandTilde(path: String): String = path.replaceFirst(Regex("^~"), System.getProperty("user.home"))

internal fun File.location(
    relative: Boolean
) = if (relative) this.toRelativeString(File(workDir)) else this.path

/**
 * Run lint over common kotlin file or kotlin script file.
 */
internal fun lintFile(
    fileName: String,
    fileContents: String,
    ruleSets: List<RuleSet>,
    userData: Map<String, String> = emptyMap(),
    editorConfigPath: String? = null,
    debug: Boolean = false,
    lintErrorCallback: (LintError) -> Unit = {}
) {
    KtLint.lint(
        KtLint.Params(
            fileName = fileName,
            text = fileContents,
            ruleSets = ruleSets,
            userData = userData,
            script = !fileName.endsWith(".kt", ignoreCase = true),
            editorConfigPath = editorConfigPath,
            cb = { e, _ ->
                lintErrorCallback(e)
            },
            debug = debug
        )
    )
}

/**
 * Format a kotlin file or script file
 */
internal fun formatFile(
    fileName: String,
    fileContents: String,
    ruleSets: Iterable<RuleSet>,
    userData: Map<String, String>,
    editorConfigPath: String?,
    debug: Boolean,
    cb: (e: LintError, corrected: Boolean) -> Unit
): String =
    KtLint.format(
        KtLint.Params(
            fileName = fileName,
            text = fileContents,
            ruleSets = ruleSets,
            userData = userData,
            script = !fileName.endsWith(".kt", ignoreCase = true),
            editorConfigPath = editorConfigPath,
            cb = cb,
            debug = debug
        )
    )

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "EXPERIMENTAL!!! Generate kotlin style section for '.editorconfig' file.",
        "Add output content into '.editorconfig' file"
    ],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class GenerateEditorConfigSubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    @OptIn(FeatureInAlphaState::class)
    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        // For now we are using CLI invocation dir as path to load existing '.editorconfig'
        val generatedEditorConfig = KtLint.generateKotlinEditorConfigSection(
            KtLint.Params(
                fileName = "./test.kt",
                text = "",
                ruleSets = ktlintCommand.rulesets
                    .loadRulesets(
                        ktlintCommand.experimental,
                        ktlintCommand.debug,
                        ktlintCommand.disabledRules
                    )
                    .map { it.value.get() },
                userData = mapOf(
                    "android" to ktlintCommand.android.toString()
                ),
                debug = ktlintCommand.debug,
                cb = { _, _ -> Unit }
            )
        )

        if (generatedEditorConfig.isNotBlank()) {
            println(
                """
                [*.{kt,kts}]
                $generatedEditorConfig
                """.trimIndent()
            )
        } else {
            println("Nothing to add to .editorconfig file")
        }
    }

    companion object {
        internal const val COMMAND_NAME = "generateEditorConfig"
    }
}

package com.pinterest.ktlint.internal

import java.io.File
import java.io.IOException
import kotlin.system.exitProcess

object GitHookInstaller {
    fun installGitHook(
        gitHookName: String,
        gitHookLoader: () -> ByteArray
    ) {
        val gitHooksDir = try {
            resolveGitHooksDir()
        } catch (e: IOException) {
            System.err.println(e.message)
            exitProcess(1)
        }

        val gitHookFile = gitHooksDir.resolve(gitHookName)
        val hookContent = gitHookLoader()

        if (gitHookFile.exists()) {
            backupExistingHook(gitHooksDir, gitHookFile, hookContent, gitHookName)
        }

        gitHookFile.writeBytes(hookContent)
        gitHookFile.setExecutable(true)
        println(".git/hooks/$gitHookName installed")
    }

    @Throws(IOException::class)
    private fun resolveGitHooksDir(): File {
        // Try to find the .git directory automatically, falling back to `./.git`
        val gitDir = try {
            val root = Runtime.getRuntime().exec("git rev-parse --show-toplevel")
                .inputStream
                .bufferedReader()
                .readText()
                .trim()

            File(root).resolve(".git")
        } catch (ex: IOException) {
            File(".git")
        }

        if (!gitDir.isDirectory) {
            throw IOException(".git directory not found. Are you sure you are inside project directory?")
        }

        val hooksDir = gitDir.resolve("hooks")
        if (!hooksDir.exists() && !hooksDir.mkdir()) {
            throw IOException("Failed to create .git/hooks folder")
        }

        return hooksDir
    }

    private fun backupExistingHook(
        hooksDir: File,
        hookFile: File,
        expectedHookContent: ByteArray,
        gitHookName: String
    ) {
        // backup existing hook (if any)
        val actualHookContent = hookFile.readBytes()
        if (actualHookContent.isNotEmpty() &&
            !actualHookContent.contentEquals(expectedHookContent)
        ) {
            val backupFile = hooksDir.resolve("$gitHookName.ktlint-backup.${actualHookContent.hex}")
            println(".git/hooks/$gitHookName -> $backupFile")
            hookFile.copyTo(backupFile, overwrite = true)
        }
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "Install git hook to automatically check files for style violations on commit",
        "Usage of \"--install-git-pre-commit-hook\" command line option is deprecated!"
    ],
    aliases = ["--install-git-pre-commit-hook"],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class GitPreCommitHookSubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        GitHookInstaller.installGitHook("pre-commit") {
            loadHookContent()
        }
    }

    private fun loadHookContent(): ByteArray = ClassLoader
        .getSystemClassLoader()
        .getResourceAsStream(
            "ktlint-git-pre-commit-hook${if (ktlintCommand.android) "-android" else ""}.sh"
        ).use { it.readBytes() }

    companion object {
        const val COMMAND_NAME = "installGitPreCommitHook"
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "Install git hook to automatically check files for style violations before push",
        "Usage of \"--install-git-pre-push-hook\" command line option is deprecated!"
    ],
    aliases = ["--install-git-pre-push-hook"],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
class GitPrePushHookSubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        GitHookInstaller.installGitHook("pre-push") {
            loadHookContent()
        }
    }

    private fun loadHookContent() = ClassLoader
        .getSystemClassLoader()
        .getResourceAsStream(
            "ktlint-git-pre-push-hook${if (ktlintCommand.android) "-android" else ""}.sh"
        )
        .readBytes()

    companion object {
        const val COMMAND_NAME = "installGitPrePushHook"
    }
}

package com.pinterest.ktlint.internal

import com.github.shyiko.klob.Glob
import com.pinterest.ktlint.core.internal.EditorConfigLoader
import com.pinterest.ktlint.core.internal.EditorConfigLoader.Companion.convertToRawValues
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.Charset
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.Paths
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.w3c.dom.Element

object IntellijIDEAIntegration {

    @Suppress("UNUSED_PARAMETER")
    @Throws(IOException::class)
    fun apply(workDir: Path, dryRun: Boolean, android: Boolean = false, local: Boolean = false): Array<Path> {
        if (!Files.isDirectory(workDir.resolve(".idea"))) {
            throw ProjectNotFoundException()
        }
        val editorConfigProperties = EditorConfigLoader(FileSystems.getDefault())
            .loadPropertiesForFile(null, isStdIn = true, rules = emptySet())
        val editorConfig: Map<String, String> = editorConfigProperties.convertToRawValues(
            null,
            isStdIn = true
        )
        val indentSize = editorConfig["indent_size"]?.toIntOrNull() ?: 4
        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(
                                "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(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").let { src ->
                    src to {
                        var arr = "<project version=\"4\"></project>".toByteArray()
                        try {
                            arr = Files.readAllBytes(Paths.get(workDir.toString(), ".idea", "workspace.xml"))
                        } catch (e: IOException) {
                            if (e !is NoSuchFileException) {
                                throw e
                            }
                        }
                        try {
                            enableOptimizeImportsOnTheFly(arr)
                        } catch (e: Exception) {
                            throw IOException("Failed to enable \"Optimize imports on the fly\" ($src)", e)
                        }
                    }
                }
            )
        } 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 for 2019.3.x and below
                Glob.from("IntelliJIdea*", "IdeaIC*", "AndroidStudio*")
                    .iterate(
                        Paths.get(home, "Library", "Preferences"),
                        Glob.IterationOption.SKIP_CHILDREN,
                        Glob.IterationOption.DIRECTORY
                    ).asSequence() +
                    // macOS for 2020.1.x and above
                    Glob.from("IntelliJIdea*", "IdeaIC*", "AndroidStudio*")
                        .iterate(
                            Paths.get(home, "Library", "Application Support", "JetBrains"),
                            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").let { src ->
                            src to {
                                var arr = "<application></application>".toByteArray()
                                try {
                                    arr = Files.readAllBytes(src)
                                } catch (e: IOException) {
                                    if (e !is NoSuchFileException) {
                                        throw e
                                    }
                                }
                                try {
                                    enableOptimizeImportsOnTheFlyInsideWorkspace(arr)
                                } catch (e: Exception) {
                                    throw IOException("Failed to enable \"Optimize imports on the fly\" ($src)", e)
                                }
                            }
                        }
                    )
                } + 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)
                Files.write(path, contentSupplier())
            }
        }
        return updates.map { (path) -> path }.toTypedArray()
    }

    private fun overwriteWithResource(resource: String, transformer: ((String) -> String) = { it }): () -> ByteArray = {
        transformer(getResourceText(resource)).toByteArray(charset("UTF-8"))
    }

    private fun enableOptimizeImportsOnTheFly(arr: ByteArray): ByteArray {
        /*
        <application>
          <component name="CodeInsightSettings">
            <option name="OPTIMIZE_IMPORTS_ON_THE_FLY" value="true" />
            ...
          </component>
          ...
        </application>
        */
        val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(ByteArrayInputStream(arr))
        val xpath = XPathFactory.newInstance().newXPath()
        var cis = xpath.evaluate(
            "//component[@name='CodeInsightSettings']",
            doc,
            XPathConstants.NODE
        ) as Element?
        if (cis == null) {
            cis = doc.createElement("component")
            cis.setAttribute("name", "CodeInsightSettings")
            cis = doc.documentElement.appendChild(cis) as Element
        }
        var oiotf = xpath.evaluate(
            "//option[@name='OPTIMIZE_IMPORTS_ON_THE_FLY']",
            cis,
            XPathConstants.NODE
        ) as Element?
        if (oiotf == null) {
            oiotf = doc.createElement("option")
            oiotf.setAttribute("name", "OPTIMIZE_IMPORTS_ON_THE_FLY")
            oiotf = cis.appendChild(oiotf) as Element
        }
        oiotf.setAttribute("value", "true")
        val transformer = TransformerFactory.newInstance().newTransformer()
        val out = ByteArrayOutputStream()
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
        transformer.transform(DOMSource(doc), StreamResult(out))
        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"))

    class ProjectNotFoundException : RuntimeException()
}

package com.pinterest.ktlint.internal

import java.util.jar.Manifest
import picocli.CommandLine

/**
 * Dynamically provides current ktlint version.
 */
internal class KtlintVersionProvider : CommandLine.IVersionProvider {
    override fun getVersion(): Array<String> {
        var version: String? = javaClass.`package`.implementationVersion
        if (version == null) {
            // 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))
            version = javaClass.getResourceAsStream("/META-INF/MANIFEST.MF")?.run {
                Manifest(this).mainAttributes.getValue("Implementation-Version")
            }
        }

        return version?.let { arrayOf(version) } ?: error("Failed to determine ktlint version")
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.KtlintCommandLine
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.ParseException
import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ruleset.test.DumpASTRule
import java.io.File
import picocli.CommandLine

@CommandLine.Command(
    description = [
        "Print AST (useful when writing/debugging rules)",
        "Usage of \"--print-ast\" command line option is deprecated!"
    ],
    aliases = ["--print-ast"],
    mixinStandardHelpOptions = true,
    versionProvider = KtlintVersionProvider::class
)
internal class PrintASTSubCommand : Runnable {
    @CommandLine.ParentCommand
    private lateinit var ktlintCommand: KtlintCommandLine

    @CommandLine.Spec
    private lateinit var commandSpec: CommandLine.Model.CommandSpec

    @CommandLine.Parameters(
        description = ["include all files under this .gitignore-like patterns"]
    )
    private var patterns = ArrayList<String>()

    @CommandLine.Option(
        names = ["--stdin"],
        description = ["Read file content from stdin"]
    )
    private var stdin: Boolean = false

    private val astRuleSet by lazy(LazyThreadSafetyMode.NONE) {
        listOf(
            RuleSet("debug", DumpASTRule(System.out, ktlintCommand.color))
        )
    }

    override fun run() {
        commandSpec.commandLine().printHelpOrVersionUsage()

        if (stdin) {
            printAST(KtLint.STDIN_FILE, String(System.`in`.readBytes()))
        } else {
            for (file in patterns.fileSequence()) {
                printAST(file.path, file.readText())
            }
        }
    }

    private fun printAST(
        fileName: String,
        fileContent: String
    ) {
        if (ktlintCommand.debug) {
            val fileLocation = if (fileName != KtLint.STDIN_FILE) {
                File(fileName).location(ktlintCommand.relative)
            } else {
                "stdin"
            }
            println("[DEBUG] Analyzing $fileLocation")
        }

        try {
            lintFile(fileName, fileContent, astRuleSet, debug = ktlintCommand.debug)
        } catch (e: Exception) {
            if (e is ParseException) {
                throw ParseException(e.line, e.col, "Not a valid Kotlin file (${e.message?.toLowerCase()})")
            }
            throw e
        }
    }

    companion object {
        internal const val COMMAND_NAME = "printAST"
    }
}

package com.pinterest.ktlint.internal

import com.pinterest.ktlint.core.RuleSetProvider
import java.net.URLClassLoader
import java.util.ServiceLoader

/**
 * Load given list of paths to rulesets jars into map of ruleset providers.
 *
 * @return map of ruleset ids to ruleset providers
 */
internal fun JarFiles.loadRulesets(
    loadExperimental: Boolean,
    debug: Boolean,
    disabledRules: String
) = ServiceLoader
    .load(
        RuleSetProvider::class.java,
        URLClassLoader(toFilesURIList().toTypedArray())
    )
    .associateBy {
        val key = it.get().id
        // standard should go first
        if (key == "standard") "\u0000$key" else key
    }
    .filterKeys { loadExperimental || it != "experimental" }
    .filterKeys { !(disabledRules.isStandardRuleSetDisabled() && it == "\u0000standard") }
    .toSortedMap()
    .also {
        if (debug) {
            it.forEach { entry ->
                println("[DEBUG] Discovered ruleset with \"${entry.key}\" id.")
            }
        }
    }

private fun String.isStandardRuleSetDisabled() =
    this.split(",").map { it.trim() }.toSet().contains("standard")

package com.github.shyiko.ktlint.core

/**
 * https://github.com/pinterest/ktlint/issues/481
 *
 * Keep the RuleSetProvider in the old package so that we can detect it and throw an error.
 */
@Deprecated("RuleSetProvider has moved to com.pinterest.ktlint.core")
interface RuleSetProvider

package com.pinterest.ktlint.core.api

@RequiresOptIn(
    message = "This Ktlint feature is highly experimental, and most probably will change in the future releases.",
    level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class FeatureInAlphaState

@RequiresOptIn(
    message = "This Ktlint feature is experimental, and may change in the future releases.",
    level = RequiresOptIn.Level.WARNING
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class FeatureInBetaState

package com.pinterest.ktlint.core.api

import org.ec4j.core.model.Property
import org.ec4j.core.model.PropertyType

/**
 * Indicates [com.pinterest.ktlint.core.Rule] uses properties loaded from `.editorconfig` file.
 *
 * This properties could be:
 * - universal `.editorconfig` properties defined
 * [here](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#current-universal-properties)
 * - universal IntelliJ IDEA properties defined
 * [here](https://github.com/JetBrains/intellij-community/blob/master/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java)
 * - Kotlin specific properties defined
 * [here](https://github.com/JetBrains/kotlin/blob/master/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java)
 *
 * In the best case rule should only use one property.
 *
 * See [com.pinterest.ktlint.core.KtLint.generateKotlinEditorConfigSection] documentation how to generate
 * `.editorconfig` based on [com.pinterest.ktlint.core.Rule]s with this interface implementations.
 */
@FeatureInAlphaState
public interface UsesEditorConfigProperties {

    /**
     * Provide a list of code style editorconfig properties, that rule uses in linting.
     */
    public val editorConfigProperties: List<EditorConfigProperty<*>>

    /**
     * Get the value of [EditorConfigProperty] based on loaded [EditorConfigProperties] content.
     */
    public fun <T> EditorConfigProperties.getEditorConfigValue(
        property: EditorConfigProperty<T>,
        isAndroidCodeStyle: Boolean = false
    ): T {
        return get(property.type.name)
            ?.getValueAs()
            ?: if (isAndroidCodeStyle) property.defaultAndroidValue else property.defaultValue
    }

    /**
     * Write the string representation of [EditorConfigProperty]
     */
    public fun <T> EditorConfigProperties.writeEditorConfigProperty(
        property: EditorConfigProperty<T>,
        isAndroidCodeStyle: Boolean
    ): String {
        return property.propertyWriter(getEditorConfigValue(property, isAndroidCodeStyle))
    }

    /**
     * Supported `.editorconfig` property.
     *
     * @param type type of property. Could be one of default ones (see [PropertyType.STANDARD_TYPES]) or custom one.
     * @param defaultValue default value for property if it does not exist in loaded properties.
     * @param defaultAndroidValue default value for android codestyle. You should set different value only when it
     * differs from [defaultValue].
     * @param propertyWriter custom function that represents [T] as String. Defaults to the standard `toString()` call.
     * You should override the default implementation in case you need a different behavior than the standard `toString()`
     * (e.g. for collections joinToString() is more applicable).
     */
    public data class EditorConfigProperty<T>(
        public val type: PropertyType<T>,
        public val defaultValue: T,
        public val defaultAndroidValue: T = defaultValue,
        public val propertyWriter: (T) -> String = { it.toString() }
    )
}

/**
 * Loaded [Property]s from `.editorconfig` files.
 */
public typealias EditorConfigProperties = Map<String, Property>

package com.pinterest.ktlint.core.ast

import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

@Suppress("unused")
object ElementType {
    val FILE: IElementType = KtStubElementTypes.FILE

    // KtNodeTypes
    val CLASS: IElementType = KtNodeTypes.CLASS
    val FUN: IElementType = KtNodeTypes.FUN
    val PROPERTY: IElementType = KtNodeTypes.PROPERTY
    val DESTRUCTURING_DECLARATION: IElementType = KtNodeTypes.DESTRUCTURING_DECLARATION
    val DESTRUCTURING_DECLARATION_ENTRY: IElementType = KtNodeTypes.DESTRUCTURING_DECLARATION_ENTRY
    val OBJECT_DECLARATION: IElementType = KtNodeTypes.OBJECT_DECLARATION
    val TYPEALIAS: IElementType = KtNodeTypes.TYPEALIAS
    val ENUM_ENTRY: IElementType = KtNodeTypes.ENUM_ENTRY
    val CLASS_INITIALIZER: IElementType = KtNodeTypes.CLASS_INITIALIZER
    val SCRIPT_INITIALIZER: IElementType = KtNodeTypes.SCRIPT_INITIALIZER
    val SECONDARY_CONSTRUCTOR: IElementType = KtNodeTypes.SECONDARY_CONSTRUCTOR
    val PRIMARY_CONSTRUCTOR: IElementType = KtNodeTypes.PRIMARY_CONSTRUCTOR
    val TYPE_PARAMETER_LIST: IElementType = KtNodeTypes.TYPE_PARAMETER_LIST
    val TYPE_PARAMETER: IElementType = KtNodeTypes.TYPE_PARAMETER
    val SUPER_TYPE_LIST: IElementType = KtNodeTypes.SUPER_TYPE_LIST
    val DELEGATED_SUPER_TYPE_ENTRY: IElementType = KtNodeTypes.DELEGATED_SUPER_TYPE_ENTRY
    val SUPER_TYPE_CALL_ENTRY: IElementType = KtNodeTypes.SUPER_TYPE_CALL_ENTRY
    val SUPER_TYPE_ENTRY: IElementType = KtNodeTypes.SUPER_TYPE_ENTRY
    val PROPERTY_DELEGATE: IElementType = KtNodeTypes.PROPERTY_DELEGATE
    val CONSTRUCTOR_CALLEE: IElementType = KtNodeTypes.CONSTRUCTOR_CALLEE
    val VALUE_PARAMETER_LIST: IElementType = KtNodeTypes.VALUE_PARAMETER_LIST
    val VALUE_PARAMETER: IElementType = KtNodeTypes.VALUE_PARAMETER
    val CLASS_BODY: IElementType = KtNodeTypes.CLASS_BODY
    val IMPORT_LIST: IElementType = KtNodeTypes.IMPORT_LIST
    val FILE_ANNOTATION_LIST: IElementType = KtNodeTypes.FILE_ANNOTATION_LIST
    val IMPORT_DIRECTIVE: IElementType = KtNodeTypes.IMPORT_DIRECTIVE
    val IMPORT_ALIAS: IElementType = KtNodeTypes.IMPORT_ALIAS
    val MODIFIER_LIST: IElementType = KtNodeTypes.MODIFIER_LIST
    val ANNOTATION: IElementType = KtNodeTypes.ANNOTATION
    val ANNOTATION_ENTRY: IElementType = KtNodeTypes.ANNOTATION_ENTRY
    val ANNOTATION_TARGET: IElementType = KtNodeTypes.ANNOTATION_TARGET
    val TYPE_ARGUMENT_LIST: IElementType = KtNodeTypes.TYPE_ARGUMENT_LIST
    val VALUE_ARGUMENT_LIST: IElementType = KtNodeTypes.VALUE_ARGUMENT_LIST
    val VALUE_ARGUMENT: IElementType = KtNodeTypes.VALUE_ARGUMENT
    val LAMBDA_ARGUMENT: IElementType = KtNodeTypes.LAMBDA_ARGUMENT
    val VALUE_ARGUMENT_NAME: IElementType = KtNodeTypes.VALUE_ARGUMENT_NAME
    val TYPE_REFERENCE: IElementType = KtNodeTypes.TYPE_REFERENCE
    val USER_TYPE: IElementType = KtNodeTypes.USER_TYPE
    val DYNAMIC_TYPE: IElementType = KtNodeTypes.DYNAMIC_TYPE
    val FUNCTION_TYPE: IElementType = KtNodeTypes.FUNCTION_TYPE
    val FUNCTION_TYPE_RECEIVER: IElementType = KtNodeTypes.FUNCTION_TYPE_RECEIVER
    val NULLABLE_TYPE: IElementType = KtNodeTypes.NULLABLE_TYPE
    val TYPE_PROJECTION: IElementType = KtNodeTypes.TYPE_PROJECTION
    val PROPERTY_ACCESSOR: IElementType = KtNodeTypes.PROPERTY_ACCESSOR
    val INITIALIZER_LIST: IElementType = KtNodeTypes.INITIALIZER_LIST
    val TYPE_CONSTRAINT_LIST: IElementType = KtNodeTypes.TYPE_CONSTRAINT_LIST
    val TYPE_CONSTRAINT: IElementType = KtNodeTypes.TYPE_CONSTRAINT
    val CONSTRUCTOR_DELEGATION_CALL: IElementType = KtNodeTypes.CONSTRUCTOR_DELEGATION_CALL
    val CONSTRUCTOR_DELEGATION_REFERENCE: IElementType = KtNodeTypes.CONSTRUCTOR_DELEGATION_REFERENCE
    val NULL: IElementType = KtNodeTypes.NULL
    val BOOLEAN_CONSTANT: IElementType = KtNodeTypes.BOOLEAN_CONSTANT
    val FLOAT_CONSTANT: IElementType = KtNodeTypes.FLOAT_CONSTANT
    val CHARACTER_CONSTANT: IElementType = KtNodeTypes.CHARACTER_CONSTANT
    val INTEGER_CONSTANT: IElementType = KtNodeTypes.INTEGER_CONSTANT
    val STRING_TEMPLATE: IElementType = KtNodeTypes.STRING_TEMPLATE
    val LONG_STRING_TEMPLATE_ENTRY: IElementType = KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY
    val SHORT_STRING_TEMPLATE_ENTRY: IElementType = KtNodeTypes.SHORT_STRING_TEMPLATE_ENTRY
    val LITERAL_STRING_TEMPLATE_ENTRY: IElementType = KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY
    val ESCAPE_STRING_TEMPLATE_ENTRY: IElementType = KtNodeTypes.ESCAPE_STRING_TEMPLATE_ENTRY
    val PARENTHESIZED: IElementType = KtNodeTypes.PARENTHESIZED
    val RETURN: IElementType = KtNodeTypes.RETURN
    val THROW: IElementType = KtNodeTypes.THROW
    val CONTINUE: IElementType = KtNodeTypes.CONTINUE
    val BREAK: IElementType = KtNodeTypes.BREAK
    val IF: IElementType = KtNodeTypes.IF
    val CONDITION: IElementType = KtNodeTypes.CONDITION
    val THEN: IElementType = KtNodeTypes.THEN
    val ELSE: IElementType = KtNodeTypes.ELSE
    val TRY: IElementType = KtNodeTypes.TRY
    val CATCH: IElementType = KtNodeTypes.CATCH
    val FINALLY: IElementType = KtNodeTypes.FINALLY
    val FOR: IElementType = KtNodeTypes.FOR
    val WHILE: IElementType = KtNodeTypes.WHILE
    val DO_WHILE: IElementType = KtNodeTypes.DO_WHILE
    val LOOP_RANGE: IElementType = KtNodeTypes.LOOP_RANGE
    val BODY: IElementType = KtNodeTypes.BODY
    val BLOCK: IElementType = KtNodeTypes.BLOCK
    val LAMBDA_EXPRESSION: IElementType = KtNodeTypes.LAMBDA_EXPRESSION
    val FUNCTION_LITERAL: IElementType = KtNodeTypes.FUNCTION_LITERAL
    val ANNOTATED_EXPRESSION: IElementType = KtNodeTypes.ANNOTATED_EXPRESSION
    val REFERENCE_EXPRESSION: IElementType = KtNodeTypes.REFERENCE_EXPRESSION
    val ENUM_ENTRY_SUPERCLASS_REFERENCE_EXPRESSION: IElementType = KtStubElementTypes.ENUM_ENTRY_SUPERCLASS_REFERENCE_EXPRESSION
    val OPERATION_REFERENCE: IElementType = KtNodeTypes.OPERATION_REFERENCE
    val LABEL: IElementType = KtNodeTypes.LABEL
    val LABEL_QUALIFIER: IElementType = KtNodeTypes.LABEL_QUALIFIER
    val THIS_EXPRESSION: IElementType = KtNodeTypes.THIS_EXPRESSION
    val SUPER_EXPRESSION: IElementType = KtNodeTypes.SUPER_EXPRESSION
    val BINARY_EXPRESSION: IElementType = KtNodeTypes.BINARY_EXPRESSION
    val BINARY_WITH_TYPE: IElementType = KtNodeTypes.BINARY_WITH_TYPE
    val IS_EXPRESSION: IElementType = KtNodeTypes.IS_EXPRESSION
    val PREFIX_EXPRESSION: IElementType = KtNodeTypes.PREFIX_EXPRESSION
    val POSTFIX_EXPRESSION: IElementType = KtNodeTypes.POSTFIX_EXPRESSION
    val LABELED_EXPRESSION: IElementType = KtNodeTypes.LABELED_EXPRESSION
    val CALL_EXPRESSION: IElementType = KtNodeTypes.CALL_EXPRESSION
    val ARRAY_ACCESS_EXPRESSION: IElementType = KtNodeTypes.ARRAY_ACCESS_EXPRESSION
    val INDICES: IElementType = KtNodeTypes.INDICES
    val DOT_QUALIFIED_EXPRESSION: IElementType = KtStubElementTypes.DOT_QUALIFIED_EXPRESSION
    val CALLABLE_REFERENCE_EXPRESSION: IElementType = KtNodeTypes.CALLABLE_REFERENCE_EXPRESSION
    val CLASS_LITERAL_EXPRESSION: IElementType = KtNodeTypes.CLASS_LITERAL_EXPRESSION
    val SAFE_ACCESS_EXPRESSION: IElementType = KtNodeTypes.SAFE_ACCESS_EXPRESSION
    val OBJECT_LITERAL: IElementType = KtNodeTypes.OBJECT_LITERAL
    val WHEN: IElementType = KtNodeTypes.WHEN
    val WHEN_ENTRY: IElementType = KtNodeTypes.WHEN_ENTRY
    val WHEN_CONDITION_IN_RANGE: IElementType = KtNodeTypes.WHEN_CONDITION_IN_RANGE
    val WHEN_CONDITION_IS_PATTERN: IElementType = KtNodeTypes.WHEN_CONDITION_IS_PATTERN
    val WHEN_CONDITION_WITH_EXPRESSION: IElementType = KtNodeTypes.WHEN_CONDITION_EXPRESSION
    val COLLECTION_LITERAL_EXPRESSION: IElementType = KtNodeTypes.COLLECTION_LITERAL_EXPRESSION
    val PACKAGE_DIRECTIVE: IElementType = KtNodeTypes.PACKAGE_DIRECTIVE
    val SCRIPT: IElementType = KtNodeTypes.SCRIPT
    val TYPE_CODE_FRAGMENT: IElementType = KtNodeTypes.TYPE_CODE_FRAGMENT
    val EXPRESSION_CODE_FRAGMENT: IElementType = KtNodeTypes.EXPRESSION_CODE_FRAGMENT
    val BLOCK_CODE_FRAGMENT: IElementType = KtNodeTypes.BLOCK_CODE_FRAGMENT

    // KtTokens
    val EOF: IElementType = KtTokens.EOF
    val RESERVED: IElementType = KtTokens.RESERVED
    val BLOCK_COMMENT: IElementType = KtTokens.BLOCK_COMMENT
    val EOL_COMMENT: IElementType = KtTokens.EOL_COMMENT
    val SHEBANG_COMMENT: IElementType = KtTokens.SHEBANG_COMMENT
    val WHITE_SPACE: IElementType = KtTokens.WHITE_SPACE
    val INTEGER_LITERAL: IElementType = KtTokens.INTEGER_LITERAL
    val FLOAT_LITERAL: IElementType = KtTokens.FLOAT_LITERAL // FLOAT_CONSTANT
    val CHARACTER_LITERAL: IElementType = KtTokens.CHARACTER_LITERAL
    val CLOSING_QUOTE: IElementType = KtTokens.CLOSING_QUOTE
    val OPEN_QUOTE: IElementType = KtTokens.OPEN_QUOTE
    val REGULAR_STRING_PART: IElementType = KtTokens.REGULAR_STRING_PART
    val ESCAPE_SEQUENCE: IElementType = KtTokens.ESCAPE_SEQUENCE
    val SHORT_TEMPLATE_ENTRY_START: IElementType = KtTokens.SHORT_TEMPLATE_ENTRY_START
    val LONG_TEMPLATE_ENTRY_START: IElementType = KtTokens.LONG_TEMPLATE_ENTRY_START
    val LONG_TEMPLATE_ENTRY_END: IElementType = KtTokens.LONG_TEMPLATE_ENTRY_END
    val DANGLING_NEWLINE: IElementType = KtTokens.DANGLING_NEWLINE
    val PACKAGE_KEYWORD: IElementType = KtTokens.PACKAGE_KEYWORD
    val AS_KEYWORD: IElementType = KtTokens.AS_KEYWORD
    val TYPEALIAS_KEYWORD: IElementType = KtTokens.TYPE_ALIAS_KEYWORD
    val CLASS_KEYWORD: IElementType = KtTokens.CLASS_KEYWORD
    val THIS_KEYWORD: IElementType = KtTokens.THIS_KEYWORD
    val SUPER_KEYWORD: IElementType = KtTokens.SUPER_KEYWORD
    val VAL_KEYWORD: IElementType = KtTokens.VAL_KEYWORD
    val VAR_KEYWORD: IElementType = KtTokens.VAR_KEYWORD
    val FUN_KEYWORD: IElementType = KtTokens.FUN_KEYWORD
    val FOR_KEYWORD: IElementType = KtTokens.FOR_KEYWORD
    val NULL_KEYWORD: IElementType = KtTokens.NULL_KEYWORD
    val TRUE_KEYWORD: IElementType = KtTokens.TRUE_KEYWORD
    val FALSE_KEYWORD: IElementType = KtTokens.FALSE_KEYWORD
    val IS_KEYWORD: IElementType = KtTokens.IS_KEYWORD
    val IN_KEYWORD: IElementType = KtTokens.IN_KEYWORD
    val THROW_KEYWORD: IElementType = KtTokens.THROW_KEYWORD
    val RETURN_KEYWORD: IElementType = KtTokens.RETURN_KEYWORD
    val BREAK_KEYWORD: IElementType = KtTokens.BREAK_KEYWORD
    val CONTINUE_KEYWORD: IElementType = KtTokens.CONTINUE_KEYWORD
    val OBJECT_KEYWORD: IElementType = KtTokens.OBJECT_KEYWORD
    val IF_KEYWORD: IElementType = KtTokens.IF_KEYWORD
    val TRY_KEYWORD: IElementType = KtTokens.TRY_KEYWORD
    val ELSE_KEYWORD: IElementType = KtTokens.ELSE_KEYWORD
    val WHILE_KEYWORD: IElementType = KtTokens.WHILE_KEYWORD
    val DO_KEYWORD: IElementType = KtTokens.DO_KEYWORD
    val WHEN_KEYWORD: IElementType = KtTokens.WHEN_KEYWORD
    val INTERFACE_KEYWORD: IElementType = KtTokens.INTERFACE_KEYWORD
    val TYPEOF_KEYWORD: IElementType = KtTokens.TYPEOF_KEYWORD
    val AS_SAFE: IElementType = KtTokens.AS_SAFE
    val IDENTIFIER: IElementType = KtTokens.IDENTIFIER
    val FIELD_IDENTIFIER: IElementType = KtTokens.FIELD_IDENTIFIER
    val LBRACKET: IElementType = KtTokens.LBRACKET
    val RBRACKET: IElementType = KtTokens.RBRACKET
    val LBRACE: IElementType = KtTokens.LBRACE
    val RBRACE: IElementType = KtTokens.RBRACE
    val LPAR: IElementType = KtTokens.LPAR
    val RPAR: IElementType = KtTokens.RPAR
    val DOT: IElementType = KtTokens.DOT
    val PLUSPLUS: IElementType = KtTokens.PLUSPLUS
    val MINUSMINUS: IElementType = KtTokens.MINUSMINUS
    val MUL: IElementType = KtTokens.MUL
    val PLUS: IElementType = KtTokens.PLUS
    val MINUS: IElementType = KtTokens.MINUS
    val EXCL: IElementType = KtTokens.EXCL
    val DIV: IElementType = KtTokens.DIV
    val PERC: IElementType = KtTokens.PERC
    val LT: IElementType = KtTokens.LT
    val GT: IElementType = KtTokens.GT
    val LTEQ: IElementType = KtTokens.LTEQ
    val GTEQ: IElementType = KtTokens.GTEQ
    val EQEQEQ: IElementType = KtTokens.EQEQEQ
    val ARROW: IElementType = KtTokens.ARROW
    val DOUBLE_ARROW: IElementType = KtTokens.DOUBLE_ARROW
    val EXCLEQEQEQ: IElementType = KtTokens.EXCLEQEQEQ
    val EQEQ: IElementType = KtTokens.EQEQ
    val EXCLEQ: IElementType = KtTokens.EXCLEQ
    val EXCLEXCL: IElementType = KtTokens.EXCLEXCL
    val ANDAND: IElementType = KtTokens.ANDAND
    val OROR: IElementType = KtTokens.OROR
    val SAFE_ACCESS: IElementType = KtTokens.SAFE_ACCESS
    val ELVIS: IElementType = KtTokens.ELVIS
    val QUEST: IElementType = KtTokens.QUEST
    val COLONCOLON: IElementType = KtTokens.COLONCOLON
    val COLON: IElementType = KtTokens.COLON
    val SEMICOLON: IElementType = KtTokens.SEMICOLON
    val DOUBLE_SEMICOLON: IElementType = KtTokens.DOUBLE_SEMICOLON
    val RANGE: IElementType = KtTokens.RANGE
    val EQ: IElementType = KtTokens.EQ
    val MULTEQ: IElementType = KtTokens.MULTEQ
    val DIVEQ: IElementType = KtTokens.DIVEQ
    val PERCEQ: IElementType = KtTokens.PERCEQ
    val PLUSEQ: IElementType = KtTokens.PLUSEQ
    val MINUSEQ: IElementType = KtTokens.MINUSEQ
    val NOT_IN: IElementType = KtTokens.NOT_IN
    val NOT_IS: IElementType = KtTokens.NOT_IS
    val HASH: IElementType = KtTokens.HASH
    val AT: IElementType = KtTokens.AT
    val COMMA: IElementType = KtTokens.COMMA
    val EOL_OR_SEMICOLON: IElementType = KtTokens.EOL_OR_SEMICOLON
    val FILE_KEYWORD: IElementType = KtTokens.FILE_KEYWORD
    val FIELD_KEYWORD: IElementType = KtTokens.FIELD_KEYWORD
    val PROPERTY_KEYWORD: IElementType = KtTokens.PROPERTY_KEYWORD
    val RECEIVER_KEYWORD: IElementType = KtTokens.RECEIVER_KEYWORD
    val PARAM_KEYWORD: IElementType = KtTokens.PARAM_KEYWORD
    val SETPARAM_KEYWORD: IElementType = KtTokens.SETPARAM_KEYWORD
    val DELEGATE_KEYWORD: IElementType = KtTokens.DELEGATE_KEYWORD
    val IMPORT_KEYWORD: IElementType = KtTokens.IMPORT_KEYWORD
    val WHERE_KEYWORD: IElementType = KtTokens.WHERE_KEYWORD
    val BY_KEYWORD: IElementType = KtTokens.BY_KEYWORD
    val GET_KEYWORD: IElementType = KtTokens.GET_KEYWORD
    val SET_KEYWORD: IElementType = KtTokens.SET_KEYWORD
    val CONSTRUCTOR_KEYWORD: IElementType = KtTokens.CONSTRUCTOR_KEYWORD
    val INIT_KEYWORD: IElementType = KtTokens.INIT_KEYWORD
    val ABSTRACT_KEYWORD: IElementType = KtTokens.ABSTRACT_KEYWORD
    val ENUM_KEYWORD: IElementType = KtTokens.ENUM_KEYWORD
    val OPEN_KEYWORD: IElementType = KtTokens.OPEN_KEYWORD
    val INNER_KEYWORD: IElementType = KtTokens.INNER_KEYWORD
    val OVERRIDE_KEYWORD: IElementType = KtTokens.OVERRIDE_KEYWORD
    val PRIVATE_KEYWORD: IElementType = KtTokens.PRIVATE_KEYWORD
    val PUBLIC_KEYWORD: IElementType = KtTokens.PUBLIC_KEYWORD
    val INTERNAL_KEYWORD: IElementType = KtTokens.INTERNAL_KEYWORD
    val PROTECTED_KEYWORD: IElementType = KtTokens.PROTECTED_KEYWORD
    val CATCH_KEYWORD: IElementType = KtTokens.CATCH_KEYWORD
    val OUT_KEYWORD: IElementType = KtTokens.OUT_KEYWORD
    val VARARG_KEYWORD: IElementType = KtTokens.VARARG_KEYWORD
    val REIFIED_KEYWORD: IElementType = KtTokens.REIFIED_KEYWORD
    val DYNAMIC_KEYWORD: IElementType = KtTokens.DYNAMIC_KEYWORD
    val COMPANION_KEYWORD: IElementType = KtTokens.COMPANION_KEYWORD
    val SEALED_KEYWORD: IElementType = KtTokens.SEALED_KEYWORD
    val DEFAULT_VISIBILITY_KEYWORD: IElementType = PUBLIC_KEYWORD
    val FINALLY_KEYWORD: IElementType = KtTokens.FINALLY_KEYWORD
    val FINAL_KEYWORD: IElementType = KtTokens.FINAL_KEYWORD
    val LATEINIT_KEYWORD: IElementType = KtTokens.LATEINIT_KEYWORD
    val DATA_KEYWORD: IElementType = KtTokens.DATA_KEYWORD
    val INLINE_KEYWORD: IElementType = KtTokens.INLINE_KEYWORD
    val NOINLINE_KEYWORD: IElementType = KtTokens.NOINLINE_KEYWORD
    val TAILREC_KEYWORD: IElementType = KtTokens.TAILREC_KEYWORD
    val EXTERNAL_KEYWORD: IElementType = KtTokens.EXTERNAL_KEYWORD
    val ANNOTATION_KEYWORD: IElementType = KtTokens.ANNOTATION_KEYWORD
    val CROSSINLINE_KEYWORD: IElementType = KtTokens.CROSSINLINE_KEYWORD
    val OPERATOR_KEYWORD: IElementType = KtTokens.OPERATOR_KEYWORD
    val INFIX_KEYWORD: IElementType = KtTokens.INFIX_KEYWORD
    val CONST_KEYWORD: IElementType = KtTokens.CONST_KEYWORD
    val SUSPEND_KEYWORD: IElementType = KtTokens.SUSPEND_KEYWORD
    val HEADER_KEYWORD: IElementType = KtTokens.HEADER_KEYWORD
    val IMPL_KEYWORD: IElementType = KtTokens.IMPL_KEYWORD
    val EXPECT_KEYWORD: IElementType = KtTokens.EXPECT_KEYWORD
    val ACTUAL_KEYWORD: IElementType = KtTokens.ACTUAL_KEYWORD

    // KDocTokens
    val KDOC: IElementType = KDocTokens.KDOC
    val KDOC_START: IElementType = KDocTokens.START
    val KDOC_END: IElementType = KDocTokens.END
    val KDOC_LEADING_ASTERISK: IElementType = KDocTokens.LEADING_ASTERISK
    val KDOC_TEXT: IElementType = KDocTokens.TEXT
    val KDOC_CODE_BLOCK_TEXT: IElementType = KDocTokens.CODE_BLOCK_TEXT
    val KDOC_TAG_NAME: IElementType = KDocTokens.TAG_NAME
    val KDOC_MARKDOWN_LINK: IElementType = KDocTokens.MARKDOWN_LINK
    val KDOC_MARKDOWN_ESCAPED_CHAR: IElementType = KDocTokens.MARKDOWN_ESCAPED_CHAR
    val KDOC_MARKDOWN_INLINE_LINK: IElementType = KDocTokens.MARKDOWN_INLINE_LINK
    val KDOC_SECTION: IElementType = KDocElementTypes.KDOC_SECTION
    val KDOC_TAG: IElementType = KDocElementTypes.KDOC_TAG
    val KDOC_NAME: IElementType = KDocElementTypes.KDOC_NAME
}

package com.pinterest.ktlint.core.ast

import com.pinterest.ktlint.core.ast.ElementType.REGULAR_STRING_PART
import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import kotlin.reflect.KClass
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.LeafElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType

fun ASTNode.nextLeaf(includeEmpty: Boolean = false, skipSubtree: Boolean = false): ASTNode? {
    var n = if (skipSubtree) this.lastChildLeafOrSelf().nextLeafAny() else this.nextLeafAny()
    if (!includeEmpty) {
        while (n != null && n.textLength == 0) {
            n = n.nextLeafAny()
        }
    }
    return n
}

fun ASTNode.nextLeaf(p: (ASTNode) -> Boolean): ASTNode? {
    var n = this.nextLeafAny()
    while (n != null && !p(n)) {
        n = n.nextLeafAny()
    }
    return n
}

private fun ASTNode.nextLeafAny(): ASTNode? {
    var n = this
    if (n.firstChildNode != null) {
        do {
            n = n.firstChildNode
        } while (n.firstChildNode != null)
        return n
    }
    return n.nextLeafStrict()
}

private fun ASTNode.nextLeafStrict(): ASTNode? {
    val nextSibling: ASTNode? = treeNext
    if (nextSibling != null) {
        return nextSibling.firstChildLeafOrSelf()
    }
    return treeParent?.nextLeafStrict()
}

fun ASTNode.firstChildLeafOrSelf(): ASTNode {
    var n = this
    if (n.firstChildNode != null) {
        do {
            n = n.firstChildNode
        } while (n.firstChildNode != null)
        return n
    }
    return n
}

fun ASTNode.prevLeaf(includeEmpty: Boolean = false): ASTNode? {
    var n = this.prevLeafAny()
    if (!includeEmpty) {
        while (n != null && n.textLength == 0) {
            n = n.prevLeafAny()
        }
    }
    return n
}

fun ASTNode.prevLeaf(p: (ASTNode) -> Boolean): ASTNode? {
    var n = this.prevLeafAny()
    while (n != null && !p(n)) {
        n = n.prevLeafAny()
    }
    return n
}

private fun ASTNode.prevLeafAny(): ASTNode? {
    val prevSibling = treePrev
    if (prevSibling != null) {
        return treePrev.lastChildLeafOrSelf()
    }
    return treeParent?.prevLeafAny()
}

fun ASTNode.lastChildLeafOrSelf(): ASTNode {
    var n = this
    if (n.lastChildNode != null) {
        do {
            n = n.lastChildNode
        } while (n.lastChildNode != null)
        return n
    }
    return n
}

fun ASTNode.prevCodeLeaf(includeEmpty: Boolean = false): ASTNode? {
    var n = prevLeaf(includeEmpty)
    while (n != null && (n.elementType == WHITE_SPACE || n.isPartOfComment())) {
        n = n.prevLeaf(includeEmpty)
    }
    return n
}

fun ASTNode.nextCodeLeaf(includeEmpty: Boolean = false, skipSubtree: Boolean = false): ASTNode? {
    var n = nextLeaf(includeEmpty, skipSubtree)
    while (n != null && (n.elementType == WHITE_SPACE || n.isPartOfComment())) {
        n = n.nextLeaf(includeEmpty, skipSubtree)
    }
    return n
}

fun ASTNode.prevCodeSibling(): ASTNode? =
    prevSibling { it.elementType != WHITE_SPACE && !it.isPartOfComment() }

inline fun ASTNode.prevSibling(p: (ASTNode) -> Boolean): ASTNode? {
    var n = this.treePrev
    while (n != null) {
        if (p(n)) {
            return n
        }
        n = n.treePrev
    }
    return null
}

fun ASTNode.nextCodeSibling(): ASTNode? =
    nextSibling { it.elementType != WHITE_SPACE && !it.isPartOfComment() }

inline fun ASTNode.nextSibling(p: (ASTNode) -> Boolean): ASTNode? {
    var n = this.treeNext
    while (n != null) {
        if (p(n)) {
            return n
        }
        n = n.treeNext
    }
    return null
}

/**
 * @param elementType [ElementType].*
 */
fun ASTNode.parent(elementType: IElementType, strict: Boolean = true): ASTNode? {
    var n: ASTNode? = if (strict) this.treeParent else this
    while (n != null) {
        if (n.elementType == elementType) {
            return n
        }
        n = n.treeParent
    }
    return null
}

fun ASTNode.parent(p: (ASTNode) -> Boolean, strict: Boolean = true): ASTNode? {
    var n: ASTNode? = if (strict) this.treeParent else this
    while (n != null) {
        if (p(n)) {
            return n
        }
        n = n.treeParent
    }
    return null
}

/**
 * @param elementType [ElementType].*
 */
fun ASTNode.isPartOf(elementType: IElementType) =
    parent(elementType, strict = false) != null

fun ASTNode.isPartOf(klass: KClass<out PsiElement>): Boolean {
    var n: ASTNode? = this
    while (n != null) {
        if (klass.java.isInstance(n.psi)) {
            return true
        }
        n = n.treeParent
    }
    return false
}

fun ASTNode.isPartOfString() =
    parent(STRING_TEMPLATE, strict = false) != null

fun ASTNode?.isWhiteSpace() =
    this != null && elementType == WHITE_SPACE
fun ASTNode?.isWhiteSpaceWithNewline() =
    this != null && elementType == WHITE_SPACE && textContains('\n')
fun ASTNode?.isWhiteSpaceWithoutNewline() =
    this != null && elementType == WHITE_SPACE && !textContains('\n')

fun ASTNode.isRoot() = elementType == ElementType.FILE
fun ASTNode.isLeaf() = firstChildNode == null

fun ASTNode.isPartOfComment() = parent({ it.psi is PsiComment }, strict = false) != null

fun ASTNode.children() =
    generateSequence(firstChildNode) { node -> node.treeNext }

fun LeafElement.upsertWhitespaceBeforeMe(text: String): LeafElement {
    val s = treePrev
    return if (s != null && s.elementType == WHITE_SPACE) {
        (s.psi as LeafElement).rawReplaceWithText(text)
    } else {
        PsiWhiteSpaceImpl(text).also { w ->
            (psi as LeafElement).rawInsertBeforeMe(w)
        }
    }
}
fun LeafElement.upsertWhitespaceAfterMe(text: String): LeafElement {
    val s = treeNext
    return if (s != null && s.elementType == WHITE_SPACE) {
        (s.psi as LeafElement).rawReplaceWithText(text)
    } else {
        PsiWhiteSpaceImpl(text).also { w ->
            (psi as LeafElement).rawInsertAfterMe(w)
        }
    }
}

fun ASTNode.visit(enter: (node: ASTNode) -> Unit) {
    enter(this)
    this.getChildren(null).forEach { it.visit(enter) }
}

fun ASTNode.visit(enter: (node: ASTNode) -> Unit, exit: (node: ASTNode) -> Unit) {
    enter(this)
    this.getChildren(null).forEach { it.visit(enter, exit) }
    exit(this)
}

fun ASTNode.lineNumber(): Int? =
    this.psi.containingFile?.viewProvider?.document?.getLineNumber(this.startOffset)?.let { it + 1 }

val ASTNode.column: Int
    get() {
        var leaf = this.prevLeaf()
        var offsetToTheLeft = 0
        while (leaf != null) {
            if ((leaf.elementType == WHITE_SPACE || leaf.elementType == REGULAR_STRING_PART) && leaf.textContains('\n')) {
                offsetToTheLeft += leaf.textLength - 1 - leaf.text.lastIndexOf('\n')
                break
            }
            offsetToTheLeft += leaf.textLength
            leaf = leaf.prevLeaf()
        }
        return offsetToTheLeft + 1
    }

fun ASTNode.lineIndent(): String {
    var leaf = this.prevLeaf()
    while (leaf != null) {
        if (leaf.elementType == WHITE_SPACE && leaf.textContains('\n')) {
            return leaf.text.substring(leaf.text.lastIndexOf('\n') + 1)
        }
        leaf = leaf.prevLeaf()
    }
    return ""
}

package com.pinterest.ktlint.core.internal

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import java.nio.file.Path
import org.ec4j.core.model.Property

/**
 * Generates Kotlin section content for `.editorconfig` file.
 *
 * Rule should implement [UsesEditorConfigProperties] interface to support this.
 */
@FeatureInAlphaState
internal class EditorConfigGenerator(
    private val editorConfigLoader: EditorConfigLoader
) {
    /**
     * Method loads merged `.editorconfig` content using [com.pinterest.ktlint.core.KtLint.Params.fileName] path,
     * and then, by querying rules from [com.pinterest.ktlint.core.KtLint.Params.ruleSets]
     * generates Kotlin section (default is `[*.{kt,kts}]`) content including expected default values.
     *
     * @return Kotlin section editorconfig content. For example:
     * ```properties
     * final-newline = true
     * indent-size = 4
     * ```
     */
    fun generateEditorconfig(
        filePath: Path,
        rules: Set<Rule>,
        isAndroidCodeStyle: Boolean = false,
        debug: Boolean = false
    ): String {
        val editorConfig: Map<String, Property> = editorConfigLoader.loadPropertiesForFile(
            filePath = filePath,
            rules = rules,
            debug = debug
        )

        return rules
            .fold(mutableMapOf<String, String?>()) { acc, rule ->
                if (rule is UsesEditorConfigProperties) {
                    if (debug) println("Checking properties for '${rule.id}' rule")
                    rule.editorConfigProperties.forEach { prop ->
                        if (debug) println("Setting '${prop.type.name}' property value")
                        acc[prop.type.name] = with(rule) {
                            editorConfig.writeEditorConfigProperty(
                                prop,
                                isAndroidCodeStyle
                            )
                        }
                    }
                }

                acc
            }
            .filterValues { it != null }
            .map {
                "${it.key} = ${it.value}"
            }
            .joinToString(separator = System.lineSeparator())
    }
}

package com.pinterest.ktlint.core.internal

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystem
import java.nio.file.Path
import org.ec4j.core.EditorConfigLoader
import org.ec4j.core.PropertyTypeRegistry
import org.ec4j.core.Resource
import org.ec4j.core.ResourcePropertiesService
import org.ec4j.core.model.Version

/**
 * Loads default `.editorconfig` and ktlint specific properties for files.
 *
 * Contains internal in-memory cache to speedup lookup.
 */
@OptIn(FeatureInAlphaState::class)
class EditorConfigLoader(
    private val fs: FileSystem
) {
    private val cache = ThreadSafeEditorConfigCache()

    /**
     * Loads applicable properties from `.editorconfig`s for given file.
     *
     * @param filePath path to file that would be checked.
     * @param isStdIn indicates that checked content comes from input.
     * Setting this to `true` overrides [filePath] and uses `.kt` pattern to load properties
     * from current folder `.editorconfig` files.
     * @param alternativeEditorConfig alternative to current [filePath] location where `.editorconfig` files should be
     * looked up
     * @param rules set of [Rule]s linting the file
     * @param debug pass `true` to enable some additional debug output
     *
     * @return all possible loaded properties applicable to given file.
     * In case file extensions is not one of [SUPPORTED_FILES] or [filePath] is `null`
     * method will immediately return empty map.
     */
    fun loadPropertiesForFile(
        filePath: Path?,
        isStdIn: Boolean = false,
        alternativeEditorConfig: Path? = null,
        rules: Set<Rule>,
        debug: Boolean = false
    ): EditorConfigProperties {
        if (!isStdIn &&
            (filePath == null || SUPPORTED_FILES.none { filePath.toString().endsWith(it) })
        ) {
            return emptyMap()
        }

        val propService = createLoaderService(rules)

        val normalizedFilePath = when {
            alternativeEditorConfig != null -> {
                val editorconfigFilePath = if (isStdIn) "stdin${SUPPORTED_FILES.first()}" else filePath!!.last()
                alternativeEditorConfig
                    .toAbsolutePath()
                    .resolve("$editorconfigFilePath")
            }
            isStdIn ->
                fs
                    .getPath(".")
                    .toAbsolutePath()
                    .resolve("stdin${SUPPORTED_FILES.first()}")
            else -> filePath
        }

        if (debug) println("Resolving .editorconfig files for $normalizedFilePath file path")

        return propService
            .queryProperties(
                Resource.Resources.ofPath(normalizedFilePath, StandardCharsets.UTF_8)
            )
            .properties
            .also {
                if (debug) {
                    val editorConfigValues = it
                        .map { entry ->
                            "${entry.key}: ${entry.value.sourceValue}"
                        }
                        .joinToString(
                            separator = ", "
                        )
                    println("Loaded .editorconfig: [$editorConfigValues]")
                }
            }
    }

    /**
     * Trims used in-memory cache.
     */
    fun trimMemory() {
        cache.clear()
    }

    private fun createLoaderService(
        rules: Set<Rule>
    ): ResourcePropertiesService {
        val propertyTypeRegistry = PropertyTypeRegistry.builder()
            .defaults()
            .apply {
                rules
                    .filterIsInstance<UsesEditorConfigProperties>()
                    .flatMap(UsesEditorConfigProperties::editorConfigProperties)
                    .forEach { prop ->
                        type(prop.type)
                    }
            }
            .build()
        val editorConfigLoader = EditorConfigLoader.of(Version.CURRENT, propertyTypeRegistry)
        return ResourcePropertiesService.builder()
            .keepUnset(true)
            .cache(cache)
            .loader(editorConfigLoader)
            .build()
    }

    companion object {
        internal const val FILE_PATH_PROPERTY = "file_path"
        /**
         * List of file extensions, editorconfig lookup will be performed.
         */
        internal val SUPPORTED_FILES = arrayOf(
            ".kt",
            ".kts"
        )

        /**
         * Converts loaded [editorConfigProperties] values to string representation.
         *
         * @param filePath added under [FILE_PATH_PROPERTY] key
         * @param isStdIn indicate that input is from std-in and [filePath] should not be added to the map
         *
         * @return converted [editorConfigProperties]
         */
        fun EditorConfigProperties.convertToRawValues(
            filePath: Path?,
            isStdIn: Boolean = false
        ): Map<String, String> {
            return if (isEmpty()) {
                emptyMap()
            } else {
                this
                    .mapValues {
                        if (it.value.isUnset) "unset" else it.value.sourceValue
                    }
                    .run {
                        if (!isStdIn) {
                            plus(FILE_PATH_PROPERTY to filePath.toString())
                        } else {
                            this
                        }
                    }
            }
        }
    }
}

package com.pinterest.ktlint.core.internal

/**
 * Provides policy to have consistent and restricted `id` field naming style.
 */
internal object IdNamingPolicy {
    private const val ID_REGEX = "[a-z]+([-][a-z]+)*"
    private val idRegex = ID_REGEX.toRegex()

    /**
     * Checks provided [id] is valid.
     *
     * Will throw [IllegalArgumentException] on invalid [id] name.
     */
    internal fun enforceNaming(id: String) =
        require(id.matches(idRegex)) { "id $id must match $ID_REGEX" }
}

package com.pinterest.ktlint.core.internal

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.mock.MockProject
import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger
import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger as DiagnosticLogger
import org.jetbrains.kotlin.com.intellij.openapi.extensions.ExtensionPoint
import org.jetbrains.kotlin.com.intellij.openapi.extensions.Extensions.getRootArea
import org.jetbrains.kotlin.com.intellij.openapi.util.UserDataHolderBase
import org.jetbrains.kotlin.com.intellij.pom.PomModel
import org.jetbrains.kotlin.com.intellij.pom.PomModelAspect
import org.jetbrains.kotlin.com.intellij.pom.PomTransaction
import org.jetbrains.kotlin.com.intellij.pom.impl.PomTransactionBase
import org.jetbrains.kotlin.com.intellij.pom.tree.TreeAspect
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.TreeCopyHandler
import org.jetbrains.kotlin.config.CompilerConfiguration
import sun.reflect.ReflectionFactory

/**
 * Initialize Kotlin Lexer.
 */
internal fun initPsiFileFactory(): PsiFileFactory {
    DiagnosticLogger.setFactory(LoggerFactory::class.java)

    val compilerConfiguration = CompilerConfiguration()
    compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)

    val project = KotlinCoreEnvironment.createForProduction(
        {},
        compilerConfiguration,
        EnvironmentConfigFiles.JVM_CONFIG_FILES
    ).project as MockProject

    project.enableASTMutations()

    return PsiFileFactory.getInstance(project)
}

/**
 * Do not print anything to the stderr when lexer is unable to match input.
 */
private 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?) {}
    }
}

/**
 * Enables AST mutations (`ktlint -F ...`).
 */
private fun MockProject.enableASTMutations() {
    val extensionPoint = "org.jetbrains.kotlin.com.intellij.treeCopyHandler"
    val extensionClassName = TreeCopyHandler::class.java.name
    for (area in arrayOf(extensionArea, getRootArea())) {
        if (!area.hasExtensionPoint(extensionPoint)) {
            area.registerExtensionPoint(extensionPoint, extensionClassName, ExtensionPoint.Kind.INTERFACE)
        }
    }

    registerService(PomModel::class.java, FormatPomModel())
}

private class FormatPomModel : UserDataHolderBase(), PomModel {

    override fun runTransaction(
        transaction: PomTransaction
    ) {
        (transaction as PomTransactionBase).run()
    }

    @Suppress("UNCHECKED_CAST")
    override fun <T : PomModelAspect> getModelAspect(
        aspect: Class<T>
    ): T? {
        if (aspect == TreeAspect::class.java) {
            // using approach described in https://git.io/vKQTo due to the magical bytecode of TreeAspect
            // (check constructor signature and compare it to the source)
            // (org.jetbrains.kotlin:kotlin-compiler-embeddable:1.0.3)
            val constructor = ReflectionFactory
                .getReflectionFactory()
                .newConstructorForSerialization(
                    aspect,
                    Any::class.java.getDeclaredConstructor(*arrayOfNulls<Class<*>>(0))
                )
            return constructor.newInstance() as T
        }
        return null
    }
}

package com.pinterest.ktlint.core.internal

internal typealias LineAndColumn = Pair<Int, Int>

/**
 * Builds function to locate position in text - line and column based on offset from the text start.
 */
internal fun buildPositionInTextLocator(
    text: String
): (offset: Int) -> LineAndColumn {
    val textLength = text.length
    val arr = ArrayList<Int>()

    var endOfLineIndex = -1
    do {
        arr.add(endOfLineIndex + 1)
        endOfLineIndex = text.indexOf('\n', endOfLineIndex + 1)
    } while (endOfLineIndex != -1)

    arr.add(textLength + if (arr.last() == textLength) 1 else 0)

    val segmentTree = SegmentTree(arr.toTypedArray())

    return { offset ->
        val line = segmentTree.indexOf(offset)
        if (line != -1) {
            val col = offset - segmentTree.get(line).left
            line + 1 to col + 1
        } else {
            1 to 1
        }
    }
}

private class SegmentTree(
    sortedArray: Array<Int>
) {

    init {
        require(sortedArray.size > 1) { "At least two data points are required" }
        sortedArray.reduce { current, next ->
            require(current <= next) { "Data points are not sorted (ASC)" }
            next
        }
    }

    private val segments: List<Segment> = sortedArray
        .dropLast(1)
        .mapIndexed { index: Int, element: Int ->
            Segment(element, sortedArray[index + 1] - 1)
        }

    fun get(i: Int): Segment = segments[i]
    fun indexOf(v: Int): Int = binarySearch(v, 0, segments.size - 1)

    private fun binarySearch(
        v: Int,
        l: Int,
        r: Int
    ): Int = when {
        l > r -> -1
        else -> {
            val i = l + (r - l) / 2
            val s = segments[i]
            if (v < s.left) {
                binarySearch(v, l, i - 1)
            } else {
                if (s.right < v) binarySearch(v, i + 1, r) else i
            }
        }
    }
}

private data class Segment(
    val left: Int,
    val right: Int
)

package com.pinterest.ktlint.core.internal

import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.visit
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.psi.KtAnnotated
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset

/**
 * Detects if given `ruleId` at given `offset` is suppressed.
 */
internal typealias SuppressionLocator = (offset: Int, ruleId: String, isRoot: Boolean) -> Boolean

/**
 * No suppression is detected. Always returns `false`.
 */
internal val noSuppression: SuppressionLocator = { _, _, _ -> false }

private val suppressAnnotationRuleMap = mapOf(
    "RemoveCurlyBracesFromTemplate" to "string-template"
)
private val suppressAnnotations = setOf("Suppress", "SuppressWarnings")
private val commentRegex = Regex("\\s")

/**
 * Builds [SuppressionLocator] for given [rootNode] of AST tree.
 */
internal fun buildSuppressedRegionsLocator(
    rootNode: ASTNode
): SuppressionLocator {
    val hintsList = collect(rootNode)
    return if (hintsList.isEmpty()) {
        noSuppression
    } else { offset, ruleId, isRoot ->
        if (isRoot) {
            val h = hintsList[0]
            h.range.last == 0 && (
                h.disabledRules.isEmpty() ||
                    h.disabledRules.contains(ruleId)
                )
        } else {
            hintsList.any { hint ->
                (
                    hint.disabledRules.isEmpty() ||
                        hint.disabledRules.contains(ruleId)
                    ) &&
                    hint.range.contains(offset)
            }
        }
    }
}

/**
 * @param range zero-based range of lines where lint errors should be suppressed
 * @param disabledRules empty set means "all"
 */
private data class SuppressionHint(
    val range: IntRange,
    val disabledRules: Set<String> = emptySet()
)

private fun collect(
    rootNode: ASTNode
): List<SuppressionHint> {
    val result = ArrayList<SuppressionHint>()
    val open = ArrayList<SuppressionHint>()
    rootNode.visit { node ->
        if (node is PsiComment) {
            val text = node.getText()
            if (text.startsWith("//")) {
                val commentText = text.removePrefix("//").trim()
                parseHintArgs(commentText, "ktlint-disable")?.let { args ->
                    val lineStart = (
                        node.prevLeaf { it is PsiWhiteSpace && it.textContains('\n') } as
                            PsiWhiteSpace?
                        )?.let { it.node.startOffset + it.text.lastIndexOf('\n') + 1 } ?: 0
                    result.add(SuppressionHint(IntRange(lineStart, node.startOffset), HashSet(args)))
                }
            } else {
                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 {
                        // match open hint
                        val disabledRules = HashSet(this)
                        val openHintIndex = open.indexOfLast { it.disabledRules == disabledRules }
                        if (openHintIndex != -1) {
                            val openingHint = open.removeAt(openHintIndex)
                            result.add(
                                SuppressionHint(
                                    IntRange(openingHint.range.first, node.startOffset),
                                    disabledRules
                                )
                            )
                        }
                    }
            }
        }
        // Extract all Suppress annotations and create SuppressionHints
        val psi = node.psi
        if (psi is KtAnnotated) {
            createSuppressionHintFromAnnotations(psi, suppressAnnotations, suppressAnnotationRuleMap)
                ?.let {
                    result.add(it)
                }
        }
    }
    result.addAll(
        open.map {
            SuppressionHint(IntRange(it.range.first, rootNode.textLength), it.disabledRules)
        }
    )
    return result
}

private fun parseHintArgs(
    commentText: String,
    key: String
): List<String>? {
    if (commentText.startsWith(key)) {
        val parsedComment = splitCommentBySpace(commentText)
        // assert exact match
        if (parsedComment[0] == key) {
            return parsedComment.tail()
        }
    }
    return null
}

private fun splitCommentBySpace(
    comment: String
) = comment
    .replace(commentRegex, " ")
    .replace(" {2,}", " ")
    .split(" ")

private fun <T> List<T>.tail() = this.subList(1, this.size)

/**
 * Creates [SuppressionHint] from annotations of given [psi]
 * Returns null if no [targetAnnotations] present or no mapping exists
 * between annotations' values and ktlint rules
 */
private fun createSuppressionHintFromAnnotations(
    psi: KtAnnotated,
    targetAnnotations: Collection<String>,
    annotationValueToRuleMapping: Map<String, String>
): SuppressionHint? = psi.annotationEntries
    .filter {
        it.calleeExpression
            ?.constructorReferenceExpression
            ?.getReferencedName() in targetAnnotations
    }
    .flatMap(KtAnnotationEntry::getValueArguments)
    .mapNotNull {
        it.getArgumentExpression()?.text?.removeSurrounding("\"")
    }
    .mapNotNull(annotationValueToRuleMapping::get)
    .let { suppressedRules ->
        if (suppressedRules.isNotEmpty()) {
            SuppressionHint(
                IntRange(psi.startOffset, psi.endOffset),
                suppressedRules.toSet()
            )
        } else {
            null
        }
    }

package com.pinterest.ktlint.core.internal

import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import org.ec4j.core.Cache
import org.ec4j.core.EditorConfigLoader
import org.ec4j.core.Resource
import org.ec4j.core.model.EditorConfig

/**
 * In-memory [Cache] implementation that could be safely accessed from any [Thread].
 */
internal class ThreadSafeEditorConfigCache : Cache {
    private val readWriteLock = ReentrantReadWriteLock()
    private val inMemoryMap = HashMap<Resource, EditorConfig>()

    override fun get(
        editorConfigFile: Resource,
        loader: EditorConfigLoader
    ): EditorConfig {
        readWriteLock.read {
            return inMemoryMap[editorConfigFile]
                ?: readWriteLock.write {
                    val result = loader.load(editorConfigFile)
                    inMemoryMap[editorConfigFile] = result
                    result
                }
        }
    }

    fun clear() = readWriteLock.write {
        inMemoryMap.clear()
    }
}

package com.pinterest.ktlint.reporter.baseline

import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.Reporter
import java.io.File
import java.io.PrintStream
import java.nio.file.Paths
import java.util.ArrayList
import java.util.concurrent.ConcurrentHashMap

class BaselineReporter(val out: PrintStream) : Reporter {

    private val acc = ConcurrentHashMap<String, MutableList<LintError>>()

    override fun onLintError(file: String, err: LintError, corrected: Boolean) {
        if (!corrected) {
            acc.getOrPut(file) { ArrayList<LintError>() }.add(err)
        }
    }

    override fun afterAll() {
        out.println("""<?xml version="1.0" encoding="utf-8"?>""")
        out.println("""<baseline version="1.0">""")
        for ((file, errList) in acc.entries.sortedBy { it.key }) {
            val fileName = try {
                val rootPath = Paths.get("").toAbsolutePath()
                val filePath = Paths.get(file)
                rootPath.relativize(filePath).toString().replace(File.separatorChar, '/')
            } catch (e: IllegalArgumentException) {
                file
            }
            out.println("""	<file name="${fileName.escapeXMLAttrValue()}">""")
            for ((line, col, ruleId, _) in errList) {
                out.println(
                    """		<error line="$line" column="$col" source="$ruleId" />"""
                )
            }
            out.println("""	</file>""")
        }
        out.println("""</baseline>""")
    }

    private fun String.escapeXMLAttrValue() =
        this.replace("&", "&").replace("\"", """).replace("'", "'")
            .replace("<", "<").replace(">", ">")
}

package com.pinterest.ktlint.reporter.baseline

import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import java.io.PrintStream

class BaselineReporterProvider : ReporterProvider {
    override val id: String = "baseline"
    override fun get(out: PrintStream, opt: Map<String, String>): Reporter = BaselineReporter(out)
}

package com.pinterest.ktlint.reporter.checkstyle

import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.Reporter
import java.io.PrintStream
import java.util.ArrayList
import java.util.concurrent.ConcurrentHashMap

class CheckStyleReporter(val out: PrintStream) : Reporter {

    private val acc = ConcurrentHashMap<String, MutableList<LintError>>()

    override fun onLintError(file: String, err: LintError, corrected: Boolean) {
        if (!corrected) {
            acc.getOrPut(file) { ArrayList<LintError>() }.add(err)
        }
    }

    override fun afterAll() {
        out.println("""<?xml version="1.0" encoding="utf-8"?>""")
        out.println("""<checkstyle version="8.0">""")
        for ((file, errList) in acc.entries.sortedBy { it.key }) {
            out.println("""	<file name="${file.escapeXMLAttrValue()}">""")
            for ((line, col, ruleId, detail) in errList) {
                out.println(
                    """		<error line="$line" column="$col" severity="error" message="${
                    detail.escapeXMLAttrValue()
                    }" source="$ruleId" />"""
                )
            }
            out.println("""	</file>""")
        }
        out.println("""</checkstyle>""")
    }

    private fun String.escapeXMLAttrValue() =
        this.replace("&", "&").replace("\"", """).replace("'", "'")
            .replace("<", "<").replace(">", ">")
}

package com.pinterest.ktlint.reporter.checkstyle

import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import java.io.PrintStream

class CheckStyleReporterProvider : ReporterProvider {
    override val id: String = "checkstyle"
    override fun get(out: PrintStream, opt: Map<String, String>): Reporter = CheckStyleReporter(out)
}

/*
 * MIT License
 *
 * Copyright (c) 2019 Matheus Candido
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.pinterest.ktlint.reporter.html

import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.Reporter
import java.io.PrintStream
import java.util.concurrent.ConcurrentHashMap

class HtmlReporter(private val out: PrintStream) : Reporter {

    private val acc = ConcurrentHashMap<String, MutableList<LintError>>()
    private var issueCount = 0
    private var correctedCount = 0

    override fun onLintError(file: String, err: LintError, corrected: Boolean) {
        if (!corrected) {
            issueCount += 1
            acc.getOrPut(file) { mutableListOf() }.add(err)
        } else {
            correctedCount += 1
        }
    }

    override fun afterAll() {
        html {
            head {
                cssLink("https://fonts.googleapis.com/css?family=Source+Code+Pro")
                text("${System.lineSeparator()}")
            }
            body {
                if (!acc.isEmpty()) {

                    h1 { text("Overview") }

                    paragraph {
                        text("Issues found: $issueCount")
                    }

                    paragraph {
                        text("Issues corrected: $correctedCount")
                    }

                    acc.forEach { file: String, errors: MutableList<LintError> ->
                        h3 { text(file) }
                        ul {
                            errors.forEach { (line, col, ruleId, detail) ->
                                item("($line, $col): $detail  ($ruleId)")
                            }
                        }
                    }
                } else {
                    paragraph {
                        text("Congratulations, no issues found!")
                    }
                }
            }
        }
    }

    private fun html(body: () -> Unit) {
        out.println("")
        body()
        out.println("")
    }

    private fun head(body: () -> Unit) {
        out.println("")
        body()
        out.println("")
    }

    private fun body(body: () -> Unit) {
        out.println("")
        body()
        out.println("")
    }

    private fun h1(body: () -> Unit) {
        out.print("
")
        body()
        out.println("
")
    }

    private fun h3(body: () -> Unit) {
        out.print("
")
        body()
        out.println("
")
    }

    private fun text(value: String) {
        out.print(value)
    }

    private fun ul(body: () -> Unit) {
        out.println("
")
        body()
        out.println("
")
    }

    private fun item(value: String) {
        out.print("
")
        text(value.escapeHTMLAttrValue())
        out.println("
")
    }

    private fun cssLink(link: String) {
        out.print("<link href=\"")
        out.print(link)
        out.println("\" rel=\"stylesheet\" />")
    }

    private fun paragraph(body: () -> Unit) {
        out.print("
")
        body()
        out.println("
")
    }

    private fun String.escapeHTMLAttrValue() =
        this.replace("&", "&").replace("\"", """).replace("'", "'")
            .replace("<", "<").replace(">", ">")
}

/*
 * MIT License
 *
 * Copyright (c) 2019 Matheus Candido
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.pinterest.ktlint.reporter.html

import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import java.io.PrintStream

class HtmlReporterProvider : ReporterProvider {
    override val id: String = "html"
    override fun get(out: PrintStream, opt: Map<String, String>): Reporter = HtmlReporter(out)
}

package com.pinterest.ktlint.reporter.json

import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.Reporter
import java.io.PrintStream
import java.util.ArrayList
import java.util.concurrent.ConcurrentHashMap

class JsonReporter(val out: PrintStream) : Reporter {

    private val acc = ConcurrentHashMap<String, MutableList<LintError>>()

    override fun onLintError(file: String, err: LintError, corrected: Boolean) {
        if (!corrected) {
            acc.getOrPut(file) { ArrayList() }.add(err)
        }
    }

    override fun afterAll() {
        out.println("[")
        val indexLast = acc.size - 1
        for ((index, entry) in acc.entries.sortedBy { it.key }.withIndex()) {
            val (file, errList) = entry
            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("]")
    }

    private fun String.escapeJsonValue() =
        this
            .replace("\\", "\\\\")
            .replace("\"", "\\\"")
            .replace("\b", "\\b")
            .replace("\n", "\\n")
            .replace("\r", "\\r")
            .replace("\t", "\\t")
}

package com.pinterest.ktlint.reporter.json

import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import java.io.PrintStream

class JsonReporterProvider : ReporterProvider {
    override val id: String = "json"
    override fun get(out: PrintStream, opt: Map<String, String>): Reporter = JsonReporter(out)
}

package com.pinterest.ktlint.reporter.plain

import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.reporter.plain.internal.Color
import com.pinterest.ktlint.reporter.plain.internal.color
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,
    val shouldColorOutput: Boolean = false,
    val outputColor: Color = Color.DARK_GRAY,
    val pad: Boolean = false
) : Reporter {

    private val acc = ConcurrentHashMap<String, MutableList<LintError>>()

    override fun onLintError(file: String, err: LintError, corrected: Boolean) {
        if (!corrected) {
            if (groupByFile) {
                acc.getOrPut(file) { ArrayList<LintError>() }.add(err)
            } else {
                out.println(
                    "${colorFileName(file)}${":".colored()}${err.line}${
                    ":${"${err.col}:".let { if (pad) String.format("%-4s", it) else it}}".colored()
                    } ${err.detail}${if (verbose) " (${err.ruleId})".colored() else ""}"
                )
            }
        }
    }

    override fun after(file: String) {
        if (groupByFile) {
            val errList = acc[file] ?: return
            out.println(colorFileName(file))
            for ((line, col, ruleId, detail) in errList) {
                out.println(
                    "  $line${
                    ":${if (pad) String.format("%-3s", col) else "$col"}".colored()
                    } $detail${if (verbose) " ($ruleId)".colored() else ""}"
                )
            }
        }
    }

    private fun colorFileName(fileName: String): String {
        val name = fileName.substringAfterLast(File.separator)
        return fileName.substring(0, fileName.length - name.length).colored() + name
    }

    private fun String.colored() =
        if (shouldColorOutput) this.color(outputColor) else this
}

package com.pinterest.ktlint.reporter.plain

import com.pinterest.ktlint.core.Reporter
import com.pinterest.ktlint.core.ReporterProvider
import com.pinterest.ktlint.reporter.plain.internal.Color
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,
            shouldColorOutput = opt["color"]?.emptyOrTrue() ?: false,
            outputColor = getColor(opt["color_name"]),
            pad = opt["pad"]?.emptyOrTrue() ?: false
        )

    private fun String.emptyOrTrue() = this == "" || this == "true"

    private fun getColor(colorInput: String?): Color {
        return Color.values().firstOrNull { it.name == colorInput } ?: throw IllegalArgumentException("Invalid color parameter.")
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.lineNumber
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.prevSibling
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
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.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtScript
import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.nextLeaf

/**
 * Ensures multiple annotations are not on the same line as the annotated declaration. Also ensures that annotations
 * with parameters are placed on separate lines.
 *
 * https://kotlinlang.org/docs/reference/coding-conventions.html#annotation-formatting
 *
 * @see [AnnotationSpacingRule] for white space rules. Moved since
 */
class AnnotationRule : Rule("annotation") {

    companion object {
        const val multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage =
            "Multiple annotations should not be placed on the same line as the annotated construct"
        const val annotationsWithParametersAreNotOnSeparateLinesErrorMessage =
            "Annotations with parameters should all be placed on separate lines prior to the annotated construct"
        const val fileAnnotationsShouldBeSeparated =
            "File annotations should be separated from file contents with a blank line"
    }

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType != MODIFIER_LIST && node.elementType != FILE_ANNOTATION_LIST) {
            return
        }

        val annotations =
            node.children()
                .mapNotNull { it.psi as? KtAnnotationEntry }
                .toList()
        if (annotations.isEmpty()) {
            return
        }

        // Join the nodes that immediately follow the annotations (whitespace), then add the final whitespace
        // if it's not a child of root. This happens when a new line separates the annotations from the annotated
        // construct. In the following example, there are no whitespace children of root, but root's next sibling is the
        // new line whitespace.
        //
        //      @JvmField
        //      val s: Any
        //
        val whiteSpaces = (annotations.asSequence().map { it.nextSibling } + node.treeNext)
            .filterIsInstance<PsiWhiteSpace>()
            .take(annotations.size)
            .toList()

        val noWhiteSpaceAfterAnnotation = node.elementType != FILE_ANNOTATION_LIST &&
            (whiteSpaces.isEmpty() || whiteSpaces.last().nextSibling is KtAnnotationEntry)
        if (noWhiteSpaceAfterAnnotation) {
            emit(
                annotations.last().endOffset - 1,
                "Missing spacing after ${annotations.last().text}",
                true
            )
            if (autoCorrect) {
                (annotations.last().nextLeaf() as? LeafPsiElement)?.upsertWhitespaceBeforeMe(" ")
            }
        }

        val multipleAnnotationsOnSameLineAsAnnotatedConstruct =
            annotations.size > 1 && !whiteSpaces.last().textContains('\n') && doesNotEndWithAComment(whiteSpaces)
        if (multipleAnnotationsOnSameLineAsAnnotatedConstruct) {
            emit(
                annotations.first().node.startOffset,
                multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage,
                true
            )
            if (autoCorrect) {
                (whiteSpaces.last() as LeafPsiElement).rawReplaceWithText(getNewlineWithIndent(node))
            }
        }

        val annotationsWithParametersAreNotOnSeparateLines =
            annotations.any { it.valueArgumentList != null } &&
                !whiteSpaces.all { it.textContains('\n') } &&
                doesNotEndWithAComment(whiteSpaces) &&
                node.treeParent.elementType != VALUE_PARAMETER && // fun fn(@Ann("blah") a: String)
                node.treeParent.elementType != VALUE_ARGUMENT && // fn(@Ann("blah") "42")
                !node.isPartOf(TYPE_ARGUMENT_LIST) && // val property: Map<@Ann("blah") String, Int>
                annotations.none { it.useSiteTarget?.getAnnotationUseSiteTarget() == AnnotationUseSiteTarget.RECEIVER }
        if (annotationsWithParametersAreNotOnSeparateLines) {
            emit(
                annotations.first().node.startOffset,
                annotationsWithParametersAreNotOnSeparateLinesErrorMessage,
                true
            )
            if (autoCorrect) {
                whiteSpaces.forEach {
                    (it as LeafPsiElement).rawReplaceWithText(getNewlineWithIndent(node))
                }
            }
        }

        if (node.elementType == FILE_ANNOTATION_LIST) {
            val lineNumber = node.lineNumber()
            val next = node.nextSibling { it.textLength > 0 }?.let { next ->
                val psi = next.psi
                ((psi as? KtScript)?.blockExpression?.firstChildNode ?: next).nextSibling {
                    !it.isWhiteSpace() && !(it.isPartOfComment() && it.lineNumber() == lineNumber)
                }
            }
            val nextLineNumber = next?.lineNumber()
            if (lineNumber != null && nextLineNumber != null) {
                val diff = nextLineNumber - lineNumber
                if (diff < 2) {
                    val psi = node.psi
                    emit(psi.endOffset - 1, fileAnnotationsShouldBeSeparated, true)
                    if (autoCorrect) {
                        if (diff == 0) {
                            psi.getNextSiblingIgnoringWhitespaceAndComments(withItself = false)?.node
                                ?.prevSibling { it.isWhiteSpace() }
                                ?.let { (it as? LeafPsiElement)?.delete() }
                            next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next)
                        }
                        next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next)
                    }
                }
            }
        }
    }

    private fun getNewlineWithIndent(modifierListRoot: ASTNode): String {
        val nodeBeforeAnnotations = modifierListRoot.treeParent.treePrev as? PsiWhiteSpace
        // If there is no whitespace before the annotation, the annotation is the first
        // text in the file
        val newLineWithIndent = nodeBeforeAnnotations?.text ?: "\n"
        return if (newLineWithIndent.contains('\n')) {
            // Make sure we only insert a single newline
            newLineWithIndent.substring(newLineWithIndent.lastIndexOf('\n'))
        } else {
            newLineWithIndent
        }
    }

    private fun doesNotEndWithAComment(whiteSpaces: List<PsiWhiteSpace>): Boolean {
        val lastNode = whiteSpaces.lastOrNull()?.nextLeaf()
        return lastNode !is PsiComment || lastNode.nextLeaf()?.textContains('\n') == false
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
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.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.endOffset

/**
 * Ensures there are not multiple line breaks between annotations and declarations.
 *
 * https://kotlinlang.org/docs/reference/coding-conventions.html#annotation-formatting
 */
class AnnotationSpacingRule : Rule("annotation-spacing") {

    companion object {
        const val fileAnnotationsLineBreaks =
            "There should not be empty lines between an annotation and the object that it's annotating"
    }

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType != ElementType.MODIFIER_LIST && node.elementType != ElementType.FILE_ANNOTATION_LIST) {
            return
        }

        val annotations =
            node.children()
                .mapNotNull { it.psi as? KtAnnotationEntry }
                .toList()
        if (annotations.isEmpty()) {
            return
        }

        // Join the nodes that immediately follow the annotations (whitespace), then add the final whitespace
        // if it's not a child of root. This happens when a new line separates the annotations from the annotated
        // construct. In the following example, there are no whitespace children of root, but root's next sibling is the
        // new line whitespace.
        //
        //      @JvmField
        //      val s: Any
        //
        val whiteSpaces = (annotations.asSequence().map { it.nextSibling } + node.treeNext)
            .filterIsInstance<PsiWhiteSpace>()
            .take(annotations.size)
            .toList()

        val next = node.nextSiblingWithAtLeastOneOf(
            {
                !it.isWhiteSpace() &&
                    it.textLength > 0 &&
                    !(it.isPartOfComment() /* && it.lineNumber() == lineNumber*/) &&
                    !it.isPartOf(ElementType.FILE_ANNOTATION_LIST)
            },
            {
                val s = it.text
                // Ensure at least one occurrence of two line breaks
                s.indexOf("\n") != s.lastIndexOf("\n")
            }
        )
        if (next != null) {
            if (node.elementType != ElementType.FILE_ANNOTATION_LIST) {
                val psi = node.psi
                emit(psi.endOffset - 1, fileAnnotationsLineBreaks, true)
                if (autoCorrect) {
                    removeExtraLineBreaks(node)
                }
            }
        }
        if (whiteSpaces.isNotEmpty() && annotations.size > 1 && node.elementType != ElementType.FILE_ANNOTATION_LIST) {
            // Check to make sure there are multi breaks between annotations
            if (whiteSpaces.any { psi -> psi.textToCharArray().filter { it == '\n' }.count() > 1 }) {
                val psi = node.psi
                emit(psi.endOffset - 1, fileAnnotationsLineBreaks, true)
                if (autoCorrect) {
                    removeIntraLineBreaks(node, annotations.last())
                }
            }
        }
    }

    private inline fun ASTNode.nextSiblingWithAtLeastOneOf(
        p: (ASTNode) -> Boolean,
        needsToOccur: (ASTNode) -> Boolean
    ): ASTNode? {
        var n = this.treeNext
        var occurrenceCount = 0
        while (n != null) {
            if (needsToOccur(n)) {
                occurrenceCount++
            }
            if (p(n)) {
                return if (occurrenceCount > 0) {
                    n
                } else {
                    null
                }
            }
            n = n.treeNext
        }
        return null
    }

    private fun removeExtraLineBreaks(node: ASTNode) {
        val next = node.nextSibling {
            it.isWhiteSpaceWithNewline()
        } as? LeafPsiElement
        if (next != null) {
            rawReplaceExtraLineBreaks(next)
        }
    }

    private fun rawReplaceExtraLineBreaks(leaf: LeafPsiElement) {
        // Replace the extra white space with a single break
        val text = leaf.text
        val firstIndex = text.indexOf("\n") + 1
        val replacementText = text.substring(0, firstIndex) +
            text.substringAfter("\n").replace("\n", "")

        leaf.rawReplaceWithText(replacementText)
    }

    private fun removeIntraLineBreaks(
        node: ASTNode,
        last: KtAnnotationEntry
    ) {
        val txt = node.text
        // Pull the next before raw replace or it will blow up
        val lNext = node.nextLeaf()
        if (node is PsiWhiteSpaceImpl) {
            if (txt.toCharArray().count { it == '\n' } > 1) {
                rawReplaceExtraLineBreaks(node)
            }
        }

        if (lNext != null && !last.text.endsWith(lNext.text)) {
            removeIntraLineBreaks(lNext, last)
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.column
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isRoot
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.lineIndent
import com.pinterest.ktlint.core.ast.lineNumber
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.visit
import kotlin.math.max
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.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.psi.KtContainerNode
import org.jetbrains.kotlin.psi.KtDoWhileExpression
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtWhileExpression
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType

/**
 * https://kotlinlang.org/docs/reference/coding-conventions.html#method-call-formatting
 *
 * The rule is more aggressive in inserting newlines after arguments than mentioned in the styleguide:
 * Each argument should be on a separate line if
 * - at least one of the arguments is
 * - maxLineLength exceeded (and separating arguments with \n would actually help)
 * in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same)
 */
class ArgumentListWrappingRule : Rule("argument-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.isRoot()) {
            val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!!
            indentSize = editorConfig.indentSize
            maxLineLength = editorConfig.maxLineLength
            return
        }
        if (indentSize <= 0) {
            return
        }

        if (node.elementType == ElementType.VALUE_ARGUMENT_LIST &&
            // skip when there are no arguments
            node.firstChildNode?.treeNext?.elementType != ElementType.RPAR &&
            // skip lambda arguments
            node.treeParent?.elementType != ElementType.FUNCTION_LITERAL &&
            // skip if number of arguments is big (we assume it with a magic number of 8)
            node.children().count { it.elementType == ElementType.VALUE_ARGUMENT } <= 8
        ) {
            // each argument should be on a separate line if
            // - at least one of the arguments is
            // - maxLineLength exceeded (and separating arguments with \n would actually help)
            // in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same)
            val putArgumentsOnSeparateLines =
                node.textContainsIgnoringLambda('\n') ||
                    // max_line_length exceeded
                    maxLineLength > -1 && (node.column - 1 + node.textLength) > maxLineLength
            if (putArgumentsOnSeparateLines) {
                // IDEA quirk:
                // generic<
                //     T,
                //     R>(
                //     1,
                //     2
                // )
                // instead of
                // generic<
                //     T,
                //     R>(
                //         1,
                //         2
                //     )
                val adjustedIndent = if (node.hasTypeParameterListInFront()) indentSize else 0

                // aiming for
                // ... LPAR
                // <line indent + indentSize> VALUE_ARGUMENT...
                // <line indent> RPAR
                val lineIndent = node.lineIndent()
                val indent = ("\n" + lineIndent.substring(0, (lineIndent.length - adjustedIndent).coerceAtLeast(0)))
                    .let { if (node.isOnSameLineAsIfKeyword()) it + " ".repeat(indentSize) else it }
                val paramIndent = indent + " ".repeat(indentSize)
                nextChild@ for (child in node.children()) {
                    when (child.elementType) {
                        ElementType.LPAR -> {
                            val prevLeaf = child.prevLeaf()
                            if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    prevLeaf.delete()
                                }
                            }
                        }
                        ElementType.VALUE_ARGUMENT,
                        ElementType.RPAR -> {
                            var argumentInnerIndentAdjustment = 0
                            val prevLeaf = child.prevWhiteSpaceWithNewLine() ?: child.prevLeaf()
                            val intendedIndent = if (child.elementType == ElementType.VALUE_ARGUMENT) {
                                paramIndent
                            } else {
                                indent
                            }
                            if (prevLeaf is PsiWhiteSpace) {
                                val spacing = prevLeaf.getText()
                                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
                                    argumentInnerIndentAdjustment = adjustedIndent.length - prevLeaf.getTextLength()
                                    (prevLeaf as LeafPsiElement).rawReplaceWithText(adjustedIndent)
                                }
                            } else {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    argumentInnerIndentAdjustment = intendedIndent.length - child.column
                                    node.addChild(PsiWhiteSpaceImpl(intendedIndent), child)
                                }
                            }
                            if (argumentInnerIndentAdjustment != 0 && child.elementType == ElementType.VALUE_ARGUMENT) {
                                child.visit { n ->
                                    if (n.elementType == ElementType.WHITE_SPACE && n.textContains('\n')) {
                                        val isInCollectionOrFunctionLiteral =
                                            n.treeParent?.elementType == ElementType.COLLECTION_LITERAL_EXPRESSION || n.treeParent?.elementType == ElementType.FUNCTION_LITERAL

                                        // If we're inside a collection literal, let's recalculate the adjustment
                                        // because the items inside the collection should not be subject to the same
                                        // indentation as the brackets.
                                        val adjustment = if (isInCollectionOrFunctionLiteral) {
                                            val expectedPosition = intendedIndent.length + indentSize
                                            expectedPosition - child.column
                                        } else {
                                            argumentInnerIndentAdjustment
                                        }

                                        val split = n.text.split("\n")
                                        (n as LeafElement).rawReplaceWithText(
                                            split.joinToString("\n") {
                                                when {
                                                    it.isEmpty() -> it
                                                    adjustment > 0 -> it + " ".repeat(adjustment)
                                                    else -> it.substring(0, max(it.length + adjustment, 0))
                                                }
                                            }
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private fun errorMessage(node: ASTNode) =
        when (node.elementType) {
            ElementType.LPAR -> """Unnecessary newline before "(""""
            ElementType.VALUE_ARGUMENT ->
                "Argument should be on a separate line (unless all arguments can fit a single line)"
            ElementType.RPAR -> """Missing newline before ")""""
            else -> throw UnsupportedOperationException()
        }

    private fun ASTNode.textContainsIgnoringLambda(char: Char): Boolean {
        return children().any { child ->
            val elementType = child.elementType
            elementType == ElementType.WHITE_SPACE && child.textContains(char) ||
                elementType == ElementType.COLLECTION_LITERAL_EXPRESSION && child.textContains(char) ||
                elementType == ElementType.VALUE_ARGUMENT && child.children().any { it.textContainsIgnoringLambda(char) }
        }
    }

    private fun ASTNode.hasTypeParameterListInFront(): Boolean =
        treeParent.children()
            .firstOrNull { it.elementType == ElementType.TYPE_PARAMETER_LIST }
            ?.children()
            ?.any { it.isWhiteSpaceWithNewline() } == true

    private fun ASTNode.prevWhiteSpaceWithNewLine(): ASTNode? {
        var prev = prevLeaf()
        while (prev != null && (prev.isWhiteSpace() || prev.isPartOfComment())) {
            if (prev.isWhiteSpaceWithNewline()) {
                return prev
            }
            prev = prev.prevLeaf()
        }
        return null
    }

    private fun ASTNode.isOnSameLineAsIfKeyword(): Boolean {
        val containerNode = psi.getStrictParentOfType<KtContainerNode>() ?: return false
        return when (val parent = containerNode.parent) {
            is KtIfExpression, is KtWhileExpression -> parent.node
            is KtDoWhileExpression -> parent.whileKeyword?.node
            else -> null
        }?.lineNumber() == lineNumber()
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
import org.jetbrains.kotlin.psi.KtEnumEntry

class EnumEntryNameCaseRule : Rule("enum-entry-name-case") {

    companion object {
        const val ERROR_MESSAGE = "Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\" or upper camel-case like \"EnumEntry\""
    }

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node !is CompositeElement) {
            return
        }
        val enumEntry = node.psi as? KtEnumEntry ?: return
        val name = enumEntry.name ?: return

        if (name.containsLowerCase()) {
            // In case of lower camel case like "enumName", or all lower case like "enumname"
            if (!name.startsWithUpperCase()) {
                emit(
                    node.startOffset,
                    ERROR_MESSAGE,
                    false
                )

                if (autoCorrect) correct(enumEntry, name)
            }

            // In case of lower case with underscore like "enum_name"
            else if (name.contains("_") && name.containsLowerCase()) {
                emit(
                    node.startOffset,
                    ERROR_MESSAGE,
                    false
                )

                if (autoCorrect) correct(enumEntry, name)
            }
        }
    }

    private fun correct(enumEntry: KtEnumEntry, originalName: String) {
        enumEntry.setName(originalName.toUpperCase())
    }

    private fun String.startsWithUpperCase(): Boolean {
        return this.isNotEmpty() && this[0].isUpperCase()
    }

    private fun String.containsLowerCase(): Boolean {
        return this.any { it.isLowerCase() }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

public class ExperimentalRuleSetProvider : RuleSetProvider {

    override fun get(): RuleSet = RuleSet(
        "experimental",
        AnnotationRule(),
        ArgumentListWrappingRule(),
        MultiLineIfElseRule(),
        NoEmptyFirstLineInMethodBlockRule(),
        PackageNameRule(),
        EnumEntryNameCaseRule(),
        SpacingAroundDoubleColonRule(),
        SpacingBetweenDeclarationsWithCommentsRule(),
        SpacingBetweenDeclarationsWithAnnotationsRule(),
        SpacingAroundAngleBracketsRule(),
        SpacingAroundUnaryOperatorRule(),
        AnnotationSpacingRule()
    )
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ELSE
import com.pinterest.ktlint.core.ast.ElementType.ELSE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.THEN
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.prevLeaf
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.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.psiUtil.leaves

/**
 * https://kotlinlang.org/docs/reference/coding-conventions.html#formatting-control-flow-statements
 *
 * TODO: if, for, when branch, do, while
 */
class MultiLineIfElseRule : Rule("multiline-if-else") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == THEN || node.elementType == ELSE) {
            if (!node.treePrev.textContains('\n')) { // if (...) <statement>
                return
            }

            if (node.firstChildNode?.firstChildNode?.elementType != LBRACE) {
                emit(node.firstChildNode.startOffset, "Missing { ... }", true)
                if (autoCorrect) {
                    autocorrect(node)
                }
            }
        }
    }

    private fun autocorrect(node: ASTNode) {
        val prevLeaves =
            node.leaves(forward = false).takeWhile { it.elementType !in listOf(RPAR, ELSE_KEYWORD) }.toList().reversed()
        val nextLeaves = node.leaves(forward = true).takeWhile { !it.isWhiteSpaceWithNewline() }.toList()
        val rightBraceIndent = node.treeParent
            .prevLeaf { it is PsiWhiteSpace && it.textContains('\n') }?.text.orEmpty()
            .let { "\n${it.substringAfterLast("\n")}" }

        (node.treePrev as LeafPsiElement).rawReplaceWithText(" ")
        KtBlockExpression(null).apply {
            val previousChild = node.firstChildNode
            node.replaceChild(node.firstChildNode, this)
            addChild(LeafPsiElement(LBRACE, "{"))
            prevLeaves.forEach(::addChild)
            addChild(previousChild)
            nextLeaves.forEach(::addChild)
            addChild(PsiWhiteSpaceImpl(rightBraceIndent))
            addChild(LeafPsiElement(RBRACE, "}"))
        }

        // Make sure else starts on same line as newly inserted right brace
        if (node.elementType == THEN && node.treeNext?.treeNext?.elementType == ELSE_KEYWORD) {
            node.treeParent.replaceChild(node.treeNext, PsiWhiteSpaceImpl(" "))
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.prevLeaf
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

class NoEmptyFirstLineInMethodBlockRule : Rule("no-empty-first-line-in-method-block") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is PsiWhiteSpace && node.textContains('\n') &&
            node.prevLeaf()?.elementType == ElementType.LBRACE && node.isPartOf(FUN) &&
            node.treeParent.elementType != CLASS_BODY // fun fn() = object : Builder {\n\n fun stuff() = Unit }
        ) {
            val split = node.getText().split("\n")
            if (split.size > 2) {
                emit(
                    node.startOffset + 1,
                    "First line in a method block should not be empty",
                    true
                )
                if (autoCorrect) {
                    (node as LeafPsiElement).rawReplaceWithText("${split.first()}\n${split.last()}")
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtPackageDirective

/**
 * @see [Kotlin Style Guide](https://kotlinlang.org/docs/reference/coding-conventions.html#naming-rules)
 * @see [Android Style Guide](https://android.github.io/kotlin-guides/style.html#package-names)
 */
class PackageNameRule : Rule("package-name") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == PACKAGE_DIRECTIVE) {
            val qualifiedName = (node.psi as KtPackageDirective).qualifiedName
            if (qualifiedName.isEmpty()) {
                return
            }
            // https://github.com/shyiko/ktlint/issues/280
/*
            val filePath = node.psi.containingFile.node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) ?: return
            val fileDir = filePath.substringBeforeLast(File.separatorChar)
                .replace('.', File.separatorChar) // kotlinc a/b.c/d/E.kt produces a/b/c/d/E.class
            if (!fileDir.endsWith(File.separatorChar + qualifiedName.replace('.', File.separatorChar))) {
                emit(node.startOffset, "Package directive doesn't match file location", false)
            }
*/
            if (qualifiedName.contains('_')) {
                emit(node.startOffset, "Package name must not contain underscore", false)
                // "package name must be in lowercase" is violated by too many to projects in the wild to forbid
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FUN_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.VAR_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement

class SpacingAroundAngleBracketsRule : Rule("spacing-around-angle-brackets") {
    private fun String.trimBeforeLastLine() = this.substring(this.lastIndexOf('\n'))

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) {
            val openingBracket = node.firstChildNode
            if (openingBracket != null) {
                // Check for rogue spacing before an opening bracket, e.g. Map <String, Int>
                val beforeLeftAngle = openingBracket.prevLeaf()
                if (beforeLeftAngle?.elementType == WHITE_SPACE) {
                    // Ignore when the whitespace is preceded by certain keywords, e.g. fun <T> func(arg: T) {}
                    if (!typesOkWithPrecedingWhitespace.contains(beforeLeftAngle.prevLeaf()?.elementType)) {
                        emit(beforeLeftAngle.startOffset, "Unexpected spacing before \"<\"", true)
                        if (autoCorrect) {
                            beforeLeftAngle.treeParent.removeChild(beforeLeftAngle)
                        }
                    }
                }

                // Check for rogue spacing after an opening bracket
                val afterLeftAngle = openingBracket.nextLeaf()
                if (afterLeftAngle?.elementType == WHITE_SPACE) {
                    if (afterLeftAngle.isWhiteSpaceWithoutNewline()) {
                        // when spacing does not include any new lines, e.g. Map< String, Int>
                        emit(afterLeftAngle.startOffset, "Unexpected spacing after \"<\"", true)
                        if (autoCorrect) {
                            afterLeftAngle.treeParent.removeChild(afterLeftAngle)
                        }
                    } else {
                        // when spacing contains at least one new line, e.g.
                        // SomeGenericType<[whitespace]
                        //
                        //      String, Int, String>
                        // gets converted to
                        // SomeGenericType<
                        //      String, Int, String>
                        val newLineWithIndent = afterLeftAngle.text.trimBeforeLastLine()
                        if (autoCorrect) {
                            (afterLeftAngle as LeafElement).rawReplaceWithText(newLineWithIndent)
                        }
                    }
                }
            }

            val closingBracket = node.lastChildNode
            if (closingBracket != null) {
                val beforeRightAngle = closingBracket.prevLeaf()
                // Check for rogue spacing before a closing bracket
                if (beforeRightAngle?.elementType == WHITE_SPACE) {
                    if (beforeRightAngle.isWhiteSpaceWithoutNewline()) {
                        // when spacing does not include any new lines, e.g. Map<String, Int >
                        emit(beforeRightAngle.startOffset, "Unexpected spacing before \">\"", true)
                        if (autoCorrect) {
                            beforeRightAngle.treeParent.removeChild(beforeRightAngle)
                        }
                    } else {
                        // when spacing contains at least one new line, e.g.
                        // SomeGenericType<String, Int, String[whitespace]
                        //
                        //      >
                        // gets converted to
                        // SomeGenericType<String, Int, String
                        //      >
                        val newLineWithIndent = beforeRightAngle.text.trimBeforeLastLine()
                        if (autoCorrect) {
                            (beforeRightAngle as LeafElement).rawReplaceWithText(newLineWithIndent)
                        }
                    }
                }
            }
        }
    }

    companion object {
        private val typesOkWithPrecedingWhitespace = setOf(VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD)
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CALLABLE_REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.CLASS_LITERAL_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.COLONCOLON
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
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

class SpacingAroundDoubleColonRule : Rule("double-colon-spacing") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == COLONCOLON) {
            val prevLeaf = node.prevLeaf()
            val nextLeaf = node.nextLeaf()

            var removeSingleWhiteSpace = false
            val spacingBefore = when {
                node.isPartOf(CLASS_LITERAL_EXPRESSION) && prevLeaf is PsiWhiteSpace -> true // Clazz::class
                node.isPartOf(CALLABLE_REFERENCE_EXPRESSION) && prevLeaf is PsiWhiteSpace -> // String::length, ::isOdd
                    if (node.treePrev == null) { // compose(length, ::isOdd), val predicate = ::isOdd
                        removeSingleWhiteSpace = true
                        !prevLeaf.textContains('\n') && prevLeaf.psi.textLength > 1
                    } else { // String::length, List<String>::isEmpty
                        !prevLeaf.textContains('\n')
                    }
                else -> false
            }

            val spacingAfter = nextLeaf is PsiWhiteSpace
            when {
                spacingBefore && spacingAfter -> {
                    emit(node.startOffset, "Unexpected spacing around \"${node.text}\"", true)
                    if (autoCorrect) {
                        prevLeaf!!.removeSelf(removeSingleWhiteSpace)
                        nextLeaf!!.treeParent.removeChild(nextLeaf)
                    }
                }
                spacingBefore -> {
                    emit(prevLeaf!!.startOffset, "Unexpected spacing before \"${node.text}\"", true)
                    if (autoCorrect) {
                        prevLeaf.removeSelf(removeSingleWhiteSpace)
                    }
                }
                spacingAfter -> {
                    emit(nextLeaf!!.startOffset, "Unexpected spacing after \"${node.text}\"", true)
                    if (autoCorrect) {
                        nextLeaf.treeParent.removeChild(nextLeaf)
                    }
                }
            }
        }
    }

    private fun ASTNode.removeSelf(removeSingleWhiteSpace: Boolean) {
        if (removeSingleWhiteSpace) {
            (this as LeafPsiElement).rawReplaceWithText(text.substring(0, textLength - 1))
        } else {
            treeParent.removeChild(this)
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpace
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
 * Ensures there are no spaces around unary operators
 *
 * @see [Kotlin Style Guide](https://kotlinlang.org/docs/reference/coding-conventions.html#horizontal-whitespace)
 */
class SpacingAroundUnaryOperatorRule : Rule("unary-op-spacing") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == ElementType.PREFIX_EXPRESSION ||
            node.elementType == ElementType.POSTFIX_EXPRESSION
        ) {
            val children = node.children().toList()

            // ignore: var a = + /* comment */ 1
            if (children.any { it.isPartOfComment() }) return

            val whiteSpace = children.firstOrNull { it.isWhiteSpace() } ?: return
            emit(whiteSpace.startOffset, "Unexpected spacing in ${node.text.replace("\n", "\\n")}", true)

            if (autoCorrect) {
                node.removeChild(whiteSpace)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.core.ast.children
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.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments

/**
 * @see https://youtrack.jetbrains.com/issue/KT-35106
 */
class SpacingBetweenDeclarationsWithAnnotationsRule : Rule("spacing-between-declarations-with-annotations") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == MODIFIER_LIST && node.hasAnnotationsAsChildren()) {
            val declaration = node.psi.parent as? KtDeclaration
            val prevDeclaration =
                declaration?.getPrevSiblingIgnoringWhitespaceAndComments(withItself = false) as? KtDeclaration
            val prevWhiteSpace = declaration?.prevSibling as? PsiWhiteSpace
            if (declaration != null && prevDeclaration != null && prevWhiteSpace?.text?.count { it == '\n' } == 1) {
                emit(
                    node.startOffset,
                    "Declarations and declarations with annotations should have an empty space between.",
                    true
                )
                if (autoCorrect) {
                    val indent = prevWhiteSpace.text.substringAfter('\n')
                    (prevWhiteSpace.node as LeafPsiElement).rawReplaceWithText("\n\n$indent")
                }
            }
        }
    }

    private fun ASTNode.hasAnnotationsAsChildren(): Boolean = children().find { it.psi is KtAnnotationEntry } != null
}

package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.prevSibling
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.psi.KtDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments

/**
 * @see https://youtrack.jetbrains.com/issue/KT-35088
 */
class SpacingBetweenDeclarationsWithCommentsRule : Rule("spacing-between-declarations-with-comments") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is PsiComment) {
            val declaration = node.parent as? KtDeclaration ?: return
            if (declaration.getPrevSiblingIgnoringWhitespaceAndComments() !is KtDeclaration) return

            val prevSibling = declaration.node.prevSibling { it.elementType != WHITE_SPACE }
            if (prevSibling != null &&
                prevSibling.elementType != FILE &&
                prevSibling !is PsiComment
            ) {
                if (declaration.prevSibling is PsiWhiteSpace && declaration.prevSibling.text.count { it == '\n' } < 2) {
                    emit(
                        node.startOffset,
                        "Declarations and declarations with comments should have an empty space between.",
                        true
                    )
                    if (autoCorrect) {
                        val indent = node.prevLeaf()?.text?.trim('\n') ?: ""
                        (declaration.prevSibling.node as LeafPsiElement).rawReplaceWithText("\n\n$indent")
                    }
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.experimental

internal fun <T> List<T>.head() = this.subList(0, this.size - 1)
internal fun <T> List<T>.tail() = this.subList(1, this.size)

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANDAND
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.DIV
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.ELSE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.ELVIS
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.MINUS
import com.pinterest.ktlint.core.ast.ElementType.MUL
import com.pinterest.ktlint.core.ast.ElementType.OROR
import com.pinterest.ktlint.core.ast.ElementType.PERC
import com.pinterest.ktlint.core.ast.ElementType.PLUS
import com.pinterest.ktlint.core.ast.ElementType.PREFIX_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
import com.pinterest.ktlint.core.ast.ElementType.WHEN_CONDITION_WITH_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.nextCodeLeaf
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevCodeLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
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.LeafElement
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

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.isPartOfComment()) {
                return
            }
            val nextLeaf = node.nextCodeLeaf()?.prevLeaf()
            if (nextLeaf?.elementType == WHITE_SPACE &&
                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.prevLeaf()
                    if (prevLeaf is PsiWhiteSpace) {
                        prevLeaf.node.treeParent.removeChild(prevLeaf.node)
                    }
                    if (!noSpaceAroundTokens.contains(elementType)) {
                        (nextLeaf as LeafElement).upsertWhitespaceAfterMe(" ")
                    }
                    node.treeParent.removeChild(node)
                    (nextLeaf as LeafElement).rawInsertAfterMe(node as LeafElement)
                }
            }
        } else if (sameLineTokens.contains(elementType) || prefixTokens.contains(elementType)) {
            if (node.isPartOfComment()) {
                return
            }
            val prevLeaf = node.prevLeaf()
            if (
                prevLeaf?.elementType == WHITE_SPACE &&
                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.nextLeaf()
                    if (nextLeaf is PsiWhiteSpace) {
                        nextLeaf.node.treeParent.removeChild(nextLeaf.node)
                    }
                    val insertionPoint = prevLeaf.prevCodeLeaf() as LeafPsiElement
                    node.treeParent.removeChild(node)
                    insertionPoint.rawInsertAfterMe(node as LeafPsiElement)
                    if (!noSpaceAroundTokens.contains(elementType)) {
                        insertionPoint.upsertWhitespaceAfterMe(" ")
                    }
                }
            }
        }
    }

    private fun ASTNode.isPartOfSpread() =
        prevCodeLeaf()?.let { leaf ->
            val type = leaf.elementType
            type == LPAR ||
                type == COMMA ||
                type == LBRACE ||
                type == ELSE_KEYWORD ||
                KtTokens.OPERATIONS.contains(type)
        } == true

    private fun ASTNode.isInPrefixPosition() =
        treeParent?.treeParent?.elementType == PREFIX_EXPRESSION

    private fun ASTNode.isPartOfWhenCondition() =
        treeParent?.treeParent?.treeParent?.elementType == WHEN_CONDITION_WITH_EXPRESSION
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
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

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 = node.prevLeaf()
            if (prevLeaf !is PsiWhiteSpace && prevLeaf is LeafPsiElement) {
                emit(node.startOffset, "Missing space before //", true)
                if (autoCorrect) {
                    node.upsertWhitespaceBeforeMe(" ")
                }
            }
            val text = node.getText()
            if (text.length != 2 &&
                !text.startsWith("// ") &&
                !text.startsWith("//noinspection") &&
                !text.startsWith("//region") &&
                !text.startsWith("//endregion") &&
                !text.startsWith("//language=")
            ) {
                emit(node.startOffset, "Missing space after //", true)
                if (autoCorrect) {
                    node.rawReplaceWithText("// " + text.removePrefix("//"))
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.CLASS
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
import com.pinterest.ktlint.core.ast.ElementType.KDOC
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
import com.pinterest.ktlint.core.ast.ElementType.SHEBANG_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.TYPEALIAS
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.prevCodeSibling
import java.nio.file.Paths
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType

/**
 * 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>(
        FILE_ANNOTATION_LIST,
        PACKAGE_DIRECTIVE,
        IMPORT_LIST,
        WHITE_SPACE,
        EOL_COMMENT,
        BLOCK_COMMENT,
        KDOC,
        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 == CLASS ||
                el.elementType == OBJECT_DECLARATION ||
                el.elementType == TYPEALIAS
            ) {
                if (className != null) {
                    // more than one class/object/typealias present
                    return
                }
                val id = el.findChildByType(IDENTIFIER)
                type = id?.prevCodeSibling()?.text
                className = id?.text
            } else if (!ignoreSet.contains(el.elementType)) {
                // https://github.com/android/android-ktx/blob/51005889235123f41492eaaecde3c623473dfe95/src/main/java/androidx/core/graphics/Path.kt case
                return
            }
        }
        if (className != null) {
            val unescapedClassName = className.replace("`", "")
            val name = Paths.get(filePath).fileName.toString().substringBefore(".")
            if (name != "package" && name != unescapedClassName) {
                emit(0, "$type $className should be declared in a file named $unescapedClassName.kt", false)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.ast.isRoot
import org.ec4j.core.model.PropertyType
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.PsiWhiteSpaceImpl

@OptIn(FeatureInAlphaState::class)
public class FinalNewlineRule :
    Rule("final-newline"),
    Rule.Modifier.RestrictToRoot,
    UsesEditorConfigProperties {

    override val editorConfigProperties: List<UsesEditorConfigProperties.EditorConfigProperty<*>> = listOf(
        insertNewLineProperty
    )

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.isRoot()) {
            if (node.textLength == 0) return
            val editorConfigProperties: EditorConfigProperties =
                node.getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY)!!
            val insertFinalNewline = editorConfigProperties.getEditorConfigValue(insertNewLineProperty)
            val lastNode = lastChildNodeOf(node)
            if (insertFinalNewline) {
                if (lastNode !is PsiWhiteSpace || !lastNode.textContains('\n')) {
                    emit(0, "File must end with a newline (\\n)", true)
                    if (autoCorrect) {
                        node.addChild(PsiWhiteSpaceImpl("\n"), null)
                    }
                }
            } else {
                if (lastNode is PsiWhiteSpace && lastNode.textContains('\n')) {
                    emit(lastNode.startOffset, "Redundant newline (\\n) at the end of file", true)
                    if (autoCorrect) {
                        lastNode.node.treeParent.removeChild(lastNode.node)
                    }
                }
            }
        }
    }

    private tailrec fun lastChildNodeOf(node: ASTNode): ASTNode? =
        if (node.lastChildNode == null) node else lastChildNodeOf(node.lastChildNode)

    internal companion object {
        internal val insertNewLineProperty = UsesEditorConfigProperties.EditorConfigProperty(
            type = PropertyType.insert_final_newline,
            defaultValue = true
        )
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.isRoot
import com.pinterest.ktlint.ruleset.standard.ImportOrderingRule.Companion.ASCII_PATTERN
import com.pinterest.ktlint.ruleset.standard.ImportOrderingRule.Companion.IDEA_PATTERN
import com.pinterest.ktlint.ruleset.standard.internal.importordering.ImportSorter
import com.pinterest.ktlint.ruleset.standard.internal.importordering.PatternEntry
import com.pinterest.ktlint.ruleset.standard.internal.importordering.parseImportsLayout
import org.ec4j.core.model.PropertyType
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.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtImportDirective

/**
 * Import ordering is configured via EditorConfig's custom property `kotlin_imports_layout`. Supported values:
 * * "idea" - default IntelliJ IDEA's order, see [IDEA_PATTERN]
 * * "ascii" - alphabetical order as recommended in Android's Kotlin style guide, see [ASCII_PATTERN]
 * * custom - defined by the following set of tokens. Tokens can be combined together in a group, groups/tokens must be comma separated:
 *  * "*" - wildcard symbol, can be used as follows:
 *      1. Single, meaning matching any import (<all other imports> in IDEA)
 *      2. After an import path, e.g. "java.*" or "kotlin.io.Closeable.*"
 *      3. Doubled after an import path, e.g. "java.**" or "kotlin.io.**", meaning "with subpackages"
 *  * "|" - blank line symbol. Only supported single blank lines between imports. Multiple blank lines will be ignored. Blank lines are not allowed outside of import list.
 *  * "^" - alias symbol, can be used as follows:
 *      1. In front of an import path, meaning matching all alias imports from this path, e.g. "^android.*"
 *      2. Alone, meaning matching any alias import - "^" (<all other alias imports> in IDEA)
 *  * import paths - these can be full paths, e.g. "java.util.List.*" as well as wildcard paths meaning "with subpackages", e.g. "kotlin.**"
 *
 * In case the custom property is not provided, the rule defaults to "ascii" style in case of "android" flag supplied, or to "idea" otherwise.
 */
@OptIn(FeatureInAlphaState::class)
public class ImportOrderingRule :
    Rule("import-ordering"),
    UsesEditorConfigProperties {

    private lateinit var importsLayout: List<PatternEntry>
    private lateinit var importSorter: ImportSorter

    public companion object {
        internal const val KTLINT_CUSTOM_IMPORTS_LAYOUT_PROPERTY_NAME = "kotlin_imports_layout"
        internal const val IDEA_IMPORTS_LAYOUT_PROPERTY_NAME = "ij_kotlin_imports_layout"
        private const val PROPERTY_DESCRIPTION = "Defines imports order layout for Kotlin files"

        /**
         * Alphabetical with capital letters before lower case letters (e.g. Z before a).
         * No blank lines between major groups (android, com, junit, net, org, java, javax).
         * Single group regardless of import type.
         *
         * https://developer.android.com/kotlin/style-guide#import_statements
         */
        private val ASCII_PATTERN = parseImportsLayout("*")

        /**
         * Default IntelliJ IDEA style: Alphabetical with capital letters before lower case letters (e.g. Z before a),
         * except such groups as "java", "javax" and "kotlin" that are placed in the end. Within the groups the alphabetical order is preserved.
         * Alias imports are placed in a separate group in the end of the list with alphabetical order inside.
         * No blank lines between groups.
         *
         * https://github.com/JetBrains/kotlin/blob/ffdab473e28d0d872136b910eb2e0f4beea2e19c/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java#L87-L91
         */
        private val IDEA_PATTERN = parseImportsLayout("*,java.**,javax.**,kotlin.**,^")

        private const val IDEA_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between " +
            "with \"java\", \"javax\", \"kotlin\" and aliases in the end"
        private const val ASCII_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between"
        private const val CUSTOM_ERROR_MESSAGE = "Imports must be ordered according to the pattern specified in .editorconfig"

        private val errorMessages = mapOf(
            IDEA_PATTERN to IDEA_ERROR_MESSAGE,
            ASCII_PATTERN to ASCII_ERROR_MESSAGE
        )

        private val editorConfigPropertyParser: (String, String?) -> PropertyType.PropertyValue<List<PatternEntry>> =
            { _, value ->
                when {
                    value.isNullOrBlank() -> PropertyType.PropertyValue.invalid(
                        value,
                        "Import layout must contain at least one entry of a wildcard symbol (*)"
                    )
                    value == "idea" -> PropertyType.PropertyValue.valid(
                        value,
                        IDEA_PATTERN
                    )
                    value == "ascii" -> PropertyType.PropertyValue.valid(
                        value,
                        ASCII_PATTERN
                    )
                    else -> try {
                        PropertyType.PropertyValue.valid(
                            value,
                            parseImportsLayout(value)
                        )
                    } catch (e: IllegalArgumentException) {
                        PropertyType.PropertyValue.invalid(
                            value,
                            "Unexpected imports layout: $value"
                        )
                    }
                }
            }

        internal val ktlintCustomImportsLayoutProperty =
            UsesEditorConfigProperties.EditorConfigProperty<List<PatternEntry>>(
                type = PropertyType(
                    KTLINT_CUSTOM_IMPORTS_LAYOUT_PROPERTY_NAME,
                    PROPERTY_DESCRIPTION,
                    editorConfigPropertyParser
                ),
                defaultValue = IDEA_PATTERN,
                defaultAndroidValue = ASCII_PATTERN,
                propertyWriter = { it.joinToString(separator = ",") }
            )

        internal val ideaImportsLayoutProperty =
            UsesEditorConfigProperties.EditorConfigProperty<List<PatternEntry>>(
                type = PropertyType(
                    IDEA_IMPORTS_LAYOUT_PROPERTY_NAME,
                    PROPERTY_DESCRIPTION,
                    editorConfigPropertyParser
                ),
                defaultValue = IDEA_PATTERN,
                defaultAndroidValue = ASCII_PATTERN,
                propertyWriter = { it.joinToString(separator = ",") }
            )
    }

    override val editorConfigProperties: List<UsesEditorConfigProperties.EditorConfigProperty<*>> = listOf(
        ktlintCustomImportsLayoutProperty,
        ideaImportsLayoutProperty,
    )

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.isRoot()) {
            val android = node.getUserData(KtLint.ANDROID_USER_DATA_KEY) ?: false
            val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY)!!
            importsLayout = editorConfig.resolveImportsLayout(android)
            importSorter = ImportSorter(importsLayout)
            return
        }

        if (node.elementType == ElementType.IMPORT_LIST) {
            val children = node.getChildren(null)
            if (children.isNotEmpty()) {
                val imports = children.filter {
                    it.elementType == ElementType.IMPORT_DIRECTIVE ||
                        it.psi is PsiWhiteSpace && it.textLength > 1 // also collect empty lines, that are represented as "\n\n"
                }
                val hasComments = children.find { it.elementType == ElementType.BLOCK_COMMENT || it.elementType == ElementType.EOL_COMMENT } != null
                val sortedImports = imports
                    .asSequence()
                    .filter { it.psi !is PsiWhiteSpace } // sorter expects KtImportDirective, whitespaces are inserted afterwards
                    .map { it.psi as KtImportDirective }
                    .sortedWith(importSorter)
                    .distinctBy { if (it.aliasName != null) it.text.substringBeforeLast(it.aliasName!!) else it.text } // distinguish by import path w/o aliases
                    .map { it.node } // transform back to ASTNode in order to operate over its method (addChild)

                // insert blank lines wherever needed
                // traverse the list using fold to have previous and current element and decide if the blank line is needed in between
                // based on the ImportSorter imported patterns and indexes from the comparator
                val sortedImportsWithSpaces = mutableListOf<ASTNode>()
                sortedImports.fold(null as ASTNode?) { prev, current ->
                    val index1 = if (prev == null) -1 else importSorter.findImportIndex((prev.psi as KtImportDirective).importPath!!)
                    val index2 = importSorter.findImportIndex((current.psi as KtImportDirective).importPath!!)

                    var hasBlankLines = false
                    for (i in (index1 + 1) until index2) {
                        if (importSorter.patterns[i] == PatternEntry.BLANK_LINE_ENTRY) {
                            hasBlankLines = true
                            break
                        }
                    }
                    if (hasBlankLines) {
                        sortedImportsWithSpaces += PsiWhiteSpaceImpl("\n\n")
                    }
                    sortedImportsWithSpaces += current

                    return@fold current
                }

                val canAutoCorrect = !hasComments
                if (!importsAreEqual(imports, sortedImportsWithSpaces) || (hasTooMuchWhitespace(children) && !isCustomLayout())) {
                    val additionalMessage = if (!canAutoCorrect) {
                        " -- no autocorrection due to comments in the import list"
                    } else {
                        ""
                    }
                    emit(
                        node.startOffset,
                        "${errorMessages.getOrDefault(importsLayout, CUSTOM_ERROR_MESSAGE)}$additionalMessage",
                        canAutoCorrect
                    )
                    if (autoCorrect && canAutoCorrect) {
                        node.removeRange(node.firstChildNode, node.lastChildNode.treeNext)
                        sortedImportsWithSpaces.reduce { current, next ->
                            node.addChild(current, null)
                            if (current !is PsiWhiteSpace && next !is PsiWhiteSpace) {
                                node.addChild(PsiWhiteSpaceImpl("\n"), null)
                            }
                            return@reduce next
                        }
                        node.addChild(sortedImportsWithSpaces.last(), null)
                    }
                }
            }
        }
    }

    private fun EditorConfigProperties.resolveImportsLayout(
        android: Boolean
    ): List<PatternEntry> = if (containsKey(KTLINT_CUSTOM_IMPORTS_LAYOUT_PROPERTY_NAME)) {
        getEditorConfigValue(ktlintCustomImportsLayoutProperty, android)
    } else {
        getEditorConfigValue(ideaImportsLayoutProperty, android)
    }

    private fun importsAreEqual(actual: List<ASTNode>, expected: List<ASTNode>): Boolean {
        if (actual.size != expected.size) return false

        val combined = actual.zip(expected)
        return combined.all { (first, second) ->
            if (first is PsiWhiteSpace && second is PsiWhiteSpace) {
                return@all (first as PsiWhiteSpace).text == (second as PsiWhiteSpace).text
            }
            return@all first == second
        }
    }

    private fun isCustomLayout() = importsLayout != IDEA_PATTERN && importsLayout != ASCII_PATTERN

    private fun hasTooMuchWhitespace(nodes: Array<ASTNode>): Boolean {
        return nodes.any { it is PsiWhiteSpace && (it as PsiWhiteSpace).text != "\n" }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.EditorConfig
import com.pinterest.ktlint.core.EditorConfig.IndentStyle
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION
import com.pinterest.ktlint.core.ast.ElementType.ARROW
import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.BINARY_WITH_TYPE
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.BODY
import com.pinterest.ktlint.core.ast.ElementType.BY_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE
import com.pinterest.ktlint.core.ast.ElementType.COLON
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.CONDITION
import com.pinterest.ktlint.core.ast.ElementType.DELEGATED_SUPER_TYPE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.ELSE
import com.pinterest.ktlint.core.ast.ElementType.ELVIS
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EQ
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.core.ast.ElementType.GT
import com.pinterest.ktlint.core.ast.ElementType.KDOC
import com.pinterest.ktlint.core.ast.ElementType.KDOC_START
import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.LBRACKET
import com.pinterest.ktlint.core.ast.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.LT
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_LITERAL
import com.pinterest.ktlint.core.ast.ElementType.OPEN_QUOTE
import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.PARENTHESIZED
import com.pinterest.ktlint.core.ast.ElementType.PROPERTY_ACCESSOR
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
import com.pinterest.ktlint.core.ast.ElementType.REGULAR_STRING_PART
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.SHORT_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_CALL_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
import com.pinterest.ktlint.core.ast.ElementType.THEN
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_CONSTRAINT_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.WHEN_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.WHERE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline
import com.pinterest.ktlint.core.ast.nextCodeLeaf
import com.pinterest.ktlint.core.ast.nextCodeSibling
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.parent
import com.pinterest.ktlint.core.ast.prevCodeLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.prevSibling
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
import com.pinterest.ktlint.core.ast.visit
import java.lang.StringBuilder
import java.util.Deque
import java.util.LinkedList
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.KtSuperTypeList

/**
 * ktlint's rule that checks & corrects indentation.
 *
 * To keep things simple, we walk the AST twice:
 * - 1st pass - insert missing newlines (e.g. between parentheses of a multi-line function call)
 * - 2st pass - correct indentation
 *
 * Current limitations:
 * - "all or nothing" (currently, rule can only be disabled for an entire file)
 */
class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast {

    private companion object {
        // run `KTLINT_DEBUG=experimental/indent ktlint ...` to enable debug output
        private val debugMode =
            (System.getenv("KTLINT_DEBUG") ?: "").split(",").contains("experimental/indent")

        private inline fun debug(msg: () -> String) {
            if (debugMode) {
                System.err.println("[DEBUG] indent: ${msg()}")
            }
        }

        private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT)
        private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT)
        private val matchingRToken =
            lTokenSet.types.zip(
                rTokenSet.types
            ).toMap()
    }

    private var line = 1
    private var expectedIndent = 0 // TODO: merge into IndentContext

    private fun reset() {
        line = 1
        expectedIndent = 0
    }

    private inline fun debug(msg: () -> String) {
        Companion.debug { "$line: " + msg() }
    }

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!!
        if (editorConfig.indentSize <= 1) {
            return
        }
        reset()
        Companion.debug { "phase: rearrangement (auto correction ${if (autoCorrect) "on" else "off"})" }
        // step 1: insert newlines (if/where needed)
        var emitted = false
        rearrange(node, autoCorrect) { offset, errorMessage, canBeAutoCorrected ->
            emitted = true
            emit(offset, errorMessage, canBeAutoCorrected)
        }
        if (emitted && autoCorrect) {
            Companion.debug {
                "indenting:\n" +
                    node.text.split("\n").mapIndexed { i, v -> "\t${i + 1}: $v" }.joinToString("\n")
            }
        }
        reset()
        Companion.debug { "phase: indentation" }
        // step 2: correct indentation
        indent(node, autoCorrect, emit, editorConfig)
    }

    private fun rearrange(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        node.visit { n ->
            when (n.elementType) {
                LPAR, LBRACE, LBRACKET -> rearrangeBlock(n, autoCorrect, emit) // TODO: LT
                SUPER_TYPE_LIST -> rearrangeSuperTypeList(n, autoCorrect, emit)
                VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> rearrangeValueList(n, autoCorrect, emit)
                ARROW -> rearrangeArrow(n, autoCorrect, emit)
                WHITE_SPACE -> line += n.text.count { it == '\n' }
            }
        }
    }

    private fun rearrangeBlock(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        val rElementType = matchingRToken[node.elementType]
        var newlineInBetween = false
        var parameterListInBetween = false
        var numberOfArgs = 0
        var firstArg: ASTNode? = null
        // matching ), ] or }
        val r = node.nextSibling {
            newlineInBetween = newlineInBetween || it.textContains('\n')
            parameterListInBetween = parameterListInBetween || it.elementType == VALUE_PARAMETER_LIST
            if (it.elementType == VALUE_ARGUMENT) {
                numberOfArgs++
                firstArg = it
            }
            it.elementType == rElementType
        }!!
        if (
            !newlineInBetween ||
            // keep { p ->
            // }
            (node.elementType == LBRACE && parameterListInBetween) ||
            // keep ({
            // }) and (object : C {
            // })
            (
                numberOfArgs == 1 &&
                    firstArg?.firstChildNode?.elementType
                    ?.let { it == OBJECT_LITERAL || it == LAMBDA_EXPRESSION } == true
                )
        ) {
            return
        }
        if (!node.nextCodeLeaf()?.prevLeaf {
            // Skip comments, whitespace, and empty nodes
            !it.isPartOfComment() &&
                !it.isWhiteSpaceWithoutNewline() &&
                it.textLength > 0
        }.isWhiteSpaceWithNewline() &&
            // IDEA quirk:
            // if (true &&
            //     true
            // ) {
            // }
            // instead of
            // if (
            //     true &&
            //     true
            // ) {
            // }
            node.treeNext?.elementType != CONDITION
        ) {
            requireNewlineAfterLeaf(node, autoCorrect, emit)
        }
        if (!r.prevLeaf().isWhiteSpaceWithNewline()) {
            requireNewlineBeforeLeaf(r, autoCorrect, emit)
        }
    }

    private fun rearrangeSuperTypeList(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        val entries = (node.psi as KtSuperTypeList).entries
        if (
            node.textContains('\n') &&
            entries.size > 1 &&
            // e.g.
            //
            // class A : B, C,
            //     D
            // or
            // class A : B, C({
            // }), D
            //
            // but not
            //
            // class A : B, C, D({
            // })
            !(
                entries.head().all { it.elementType == SUPER_TYPE_ENTRY } &&
                    entries.last().elementType == SUPER_TYPE_CALL_ENTRY
                )
        ) {
            // put space after :
            if (!node.prevLeaf().isWhiteSpaceWithNewline()) {
                val colon = node.prevCodeLeaf()!!
                if (
                    !colon.prevLeaf().isWhiteSpaceWithNewline() &&
                    colon.prevCodeLeaf().let { it?.elementType != RPAR || !it.prevLeaf().isWhiteSpaceWithNewline() }
                ) {
                    requireNewlineAfterLeaf(colon, autoCorrect, emit)
                }
            }
            // put entries on separate lines
            // TODO: group emit()s below with the one above into one (similar to ParameterListWrappingRule)
            for (c in node.children()) {
                if (c.elementType == COMMA && !c.treeNext.isWhiteSpaceWithNewline()) {
                    requireNewlineAfterLeaf(c, autoCorrect, emit)
                }
            }
        }
    }

    private fun rearrangeValueList(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        for (c in node.children()) {
            if (
                (c.elementType == VALUE_PARAMETER || c.elementType == VALUE_ARGUMENT || c.elementType == ANNOTATION) &&
                c.textContains('\n')
            ) {
                // rearrange
                //
                // a, b, value(
                // ), c, d
                //
                // to
                //
                // a, b,
                // value(
                // ),
                // c, d

                // insert \n in front of multi-line value
                val prevSibling = c.prevSibling { it.elementType != WHITE_SPACE }
                if (
                    prevSibling?.elementType == COMMA &&
                    !prevSibling.treeNext.isWhiteSpaceWithNewline()
                ) {
                    requireNewlineAfterLeaf(prevSibling, autoCorrect, emit)
                }
                // insert \n after multi-line value
                val nextSibling = c.nextSibling { it.elementType != WHITE_SPACE }
                if (
                    nextSibling?.elementType == COMMA &&
                    !nextSibling.treeNext.isWhiteSpaceWithNewline() &&
                    // value(
                    // ), // a comment
                    // c, d
                    nextSibling.treeNext?.treeNext?.psi !is PsiComment
                ) {
                    requireNewlineAfterLeaf(nextSibling, autoCorrect, emit)
                }
            }
        }
    }

    private fun mustBeFollowedByNewline(node: ASTNode): Boolean {
        // find EOL token (last token before \n)
        // if token is in lTokenSet
        //     find matching rToken
        //     return true if there is no newline after the rToken
        // return false
        val p = node.treeParent
        val nextCodeSibling = node.nextCodeSibling() // e.g. BINARY_EXPRESSION
        var lToken = nextCodeSibling?.nextLeaf { it.isWhiteSpaceWithNewline() }?.prevCodeLeaf()
        if (lToken != null && lToken.elementType !in lTokenSet) {
            // special cases:
            // x = y.f({ z ->
            // })
            // x = y.f(0, 1,
            // 2, 3)
            lToken = lToken.prevLeaf { it.elementType in lTokenSet || it == node }
        }
        if (lToken != null && lToken.elementType in lTokenSet) {
            val rElementType = matchingRToken[lToken.elementType]
            val rToken = lToken.nextSibling { it.elementType == rElementType }
            return rToken?.treeParent == lToken.treeParent
        }
        if (nextCodeSibling?.textContains('\n') == false) {
            return true
        }
        return false
    }

    private fun rearrangeArrow(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        val p = node.treeParent
        if (
            // check
            // `{ p -> ... }`
            // and
            // `when { m -> ... }`
            // only
            p.elementType.let { it != FUNCTION_LITERAL && it != WHEN_ENTRY } ||
            // ... and only if expression after -> spans multiple lines
            !p.textContains('\n') ||
            // permit
            // when {
            //     m -> 0 + d({
            //     })
            // }
            (p.elementType == WHEN_ENTRY && mustBeFollowedByNewline(node)) ||
            // permit
            // when (this) {
            //     in 0x1F600..0x1F64F, // Emoticons
            //     0x200D // Zero-width Joiner
            //     -> true
            // }
            (p.elementType == WHEN_ENTRY && node.prevLeaf()?.textContains('\n') == true)
        ) {
            return
        }
        if (!node.nextCodeLeaf()?.prevLeaf().isWhiteSpaceWithNewline()) {
            requireNewlineAfterLeaf(node, autoCorrect, emit)
        }
        val r = node.nextSibling { it.elementType == RBRACE } ?: return
        if (!r.prevLeaf().isWhiteSpaceWithNewline()) {
            requireNewlineBeforeLeaf(r, autoCorrect, emit)
        }
    }

    private fun requireNewlineBeforeLeaf(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        emit(
            node.startOffset - 1,
            """Missing newline before "${node.text}"""",
            true
        )
        debug { (if (!autoCorrect) "would have " else "") + "inserted newline before ${node.text}" }
        if (autoCorrect) {
            (node.psi as LeafPsiElement).upsertWhitespaceBeforeMe("\n ")
        }
    }

    private fun requireNewlineAfterLeaf(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        emit(
            node.startOffset + 1,
            """Missing newline after "${node.text}"""",
            true
        )
        debug { (if (!autoCorrect) "would have " else "") + "inserted newline after ${node.text}" }
        if (autoCorrect) {
            (node.psi as LeafPsiElement).upsertWhitespaceAfterMe("\n ")
        }
    }

    private class IndentContext {
        private val exitAdj = mutableMapOf<ASTNode, Int>()
        val ignored = mutableSetOf<ASTNode>()
        val blockOpeningLineStack: Deque<Int> = LinkedList<Int>()
        var localAdj: Int = 0
        fun exitAdjBy(node: ASTNode, change: Int) {
            exitAdj.compute(node) { _, v -> (v ?: 0) + change }
        }
        fun clearExitAdj(node: ASTNode): Int? =
            exitAdj.remove(node)
    }

    private fun indent(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
        editorConfig: EditorConfig
    ) {
        val firstNotEmptyLeaf = node.nextLeaf()
        if (firstNotEmptyLeaf?.let { it.elementType == WHITE_SPACE && !it.textContains('\n') } == true) {
            visitWhiteSpace(firstNotEmptyLeaf, autoCorrect, emit, editorConfig)
        }
        val ctx = IndentContext()
        node.visit(
            { n ->
                when (n.elementType) {
                    LPAR, LBRACE, LBRACKET -> {
                        // ({[ should increase expectedIndent by 1
                        val prevBlockLine = ctx.blockOpeningLineStack.peek() ?: -1
                        if (prevBlockLine != line) {
                            expectedIndent++
                            debug { "++${n.text} -> $expectedIndent" }
                        }
                        ctx.blockOpeningLineStack.push(line)
                    }
                    RPAR, RBRACE, RBRACKET -> {
                        // ]}) should decrease expectedIndent by 1
                        val blockLine = ctx.blockOpeningLineStack.pop()
                        val prevBlockLine = ctx.blockOpeningLineStack.peek() ?: -1
                        if (prevBlockLine != blockLine) {
                            expectedIndent--
                            debug { "--${n.text} -> $expectedIndent" }
                        }
                    }
                    LT ->
                        // <T>
                        if (n.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) {
                            expectedIndent++
                            debug { "++${n.text} -> $expectedIndent" }
                        }
                    GT ->
                        // <T>
                        if (n.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) {
                            expectedIndent--
                            debug { "--${n.text} -> $expectedIndent" }
                        }
                    SUPER_TYPE_LIST ->
                        // class A :
                        //     SUPER_TYPE_LIST
                        adjustExpectedIndentInsideSuperTypeList(n)
                    SUPER_TYPE_CALL_ENTRY, DELEGATED_SUPER_TYPE_ENTRY -> {
                        // IDEA quirk:
                        //
                        // class A : B({
                        //     f() {}
                        // }),
                        //     C({
                        //         f() {}
                        //     })
                        //
                        // instead of expected
                        //
                        // class A : B({
                        //         f() {}
                        //     }),
                        //     C({
                        //         f() {}
                        //     })
                        adjustExpectedIndentInsideSuperTypeCall(n, ctx)
                    }
                    STRING_TEMPLATE ->
                        indentStringTemplate(n, autoCorrect, emit, editorConfig)
                    DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE -> {
                        val prevBlockLine = ctx.blockOpeningLineStack.peek() ?: -1
                        if (prevBlockLine == line) {
                            ctx.ignored.add(n)
                        }
                    }
                    WHITE_SPACE ->
                        if (n.textContains('\n')) {
                            if (
                                !n.isPartOfComment() &&
                                !n.isPartOfTypeConstraint() // FIXME IndentationRuleTest.testLintWhereClause not checked
                            ) {
                                val p = n.treeParent
                                val nextSibling = n.treeNext
                                val prevLeaf = n.prevLeaf { !it.isPartOfComment() && !it.isWhiteSpaceWithoutNewline() }
                                when {
                                    p.elementType.let {
                                        it == DOT_QUALIFIED_EXPRESSION || it == SAFE_ACCESS_EXPRESSION
                                    } ->
                                        // value
                                        //     .x()
                                        //     .y
                                        adjustExpectedIndentInsideQualifiedExpression(n, ctx)
                                    p.elementType.let {
                                        it == BINARY_EXPRESSION || it == BINARY_WITH_TYPE
                                    } ->
                                        // value
                                        //     + x()
                                        //     + y
                                        adjustExpectedIndentInsideBinaryExpression(n, ctx)
                                    nextSibling?.elementType.let {
                                        it == THEN || it == ELSE || it == BODY
                                    } ->
                                        // if (...)
                                        //     THEN
                                        // else
                                        //     ELSE
                                        // while (...)
                                        //     BODY
                                        adjustExpectedIndentInFrontOfControlBlock(n, ctx)
                                    nextSibling?.elementType == PROPERTY_ACCESSOR ->
                                        // val f: Type =
                                        //     PROPERTY_ACCESSOR get() = ...
                                        //     PROPERTY_ACCESSOR set() = ...
                                        adjustExpectedIndentInFrontOfPropertyAccessor(n, ctx)
                                    nextSibling?.elementType == SUPER_TYPE_LIST ->
                                        // class C :
                                        //     SUPER_TYPE_LIST
                                        adjustExpectedIndentInFrontOfSuperTypeList(n, ctx)
                                    prevLeaf?.elementType == EQ && p.elementType != VALUE_ARGUMENT ->
                                        // v =
                                        //     value
                                        adjustExpectedIndentAfterEq(n, ctx)
                                    prevLeaf?.elementType == ARROW ->
                                        // when {
                                        //    v ->
                                        //        value
                                        // }
                                        adjustExpectedIndentAfterArrow(n, ctx)
                                    prevLeaf?.elementType == COLON ->
                                        // fun fn():
                                        //     Int
                                        adjustExpectedIndentAfterColon(n, ctx)
                                    prevLeaf?.elementType == LPAR &&
                                        p.elementType == VALUE_ARGUMENT_LIST &&
                                        p.parent(CONDITION)?.takeIf { !it.prevLeaf().isWhiteSpaceWithNewline() } != null ->
                                        // if (condition(
                                        //         params
                                        //     )
                                        // )
                                        adjustExpectedIndentAfterLparInsideCondition(n, ctx)
                                }
                                visitWhiteSpace(n, autoCorrect, emit, editorConfig)
                                if (ctx.localAdj != 0) {
                                    expectedIndent += ctx.localAdj
                                    ctx.localAdj = 0
                                }
                            } else if (n.isPartOf(KDOC)) {
                                visitWhiteSpace(n, autoCorrect, emit, editorConfig)
                            }
                            line += n.text.count { it == '\n' }
                        }
                    EOL_COMMENT ->
                        if (debugMode && n.text == "// ktlint-debug-print-expected-indent") {
                            debug { "expected indent: $expectedIndent" }
                        }
                }
            },
            { n ->
                when (n.elementType) {
                    SUPER_TYPE_LIST ->
                        adjustExpectedIndentAfterSuperTypeList(n)
                    DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE ->
                        ctx.ignored.remove(n)
                }
                val adj = ctx.clearExitAdj(n)
                if (adj != null) {
                    expectedIndent += adj
                    debug { "adjusted ${n.elementType} by $adj -> $expectedIndent" }
                }
            }
        )
    }

    private fun adjustExpectedIndentInsideQualifiedExpression(n: ASTNode, ctx: IndentContext) {
        val p = n.parent({
            it.treeParent.elementType != DOT_QUALIFIED_EXPRESSION && it.treeParent.elementType != SAFE_ACCESS_EXPRESSION
        }) ?: return
        val nextSibling = n.treeNext
        if (!ctx.ignored.contains(p) && nextSibling != null) {
            expectedIndent++
            debug { "++inside(${p.elementType}) -> $expectedIndent" }
            val siblingType = nextSibling.elementType
            val e = if (
                siblingType == DOT ||
                siblingType == SAFE_ACCESS ||
                siblingType == ELVIS
            ) {
                nextSibling.treeNext
            } else {
                nextSibling
            }
            ctx.ignored.add(p)
            ctx.exitAdjBy(p, -1)
        }
    }

    private fun adjustExpectedIndentInsideBinaryExpression(n: ASTNode, ctx: IndentContext) {
        if (!n.isPartOfCondition()) {
            val p = n.treeParent
            if (!ctx.ignored.contains(p)) {
                expectedIndent++
                debug { "++inside(${p.elementType}) -> $expectedIndent" }
                val rOperand = n.nextSibling { sibling ->
                    sibling.elementType != OPERATION_REFERENCE &&
                        sibling.elementType != WHITE_SPACE
                }!!
                ctx.exitAdjBy(rOperand, -1)
            }
            val nextSibling = n.treeNext
            if (
                nextSibling?.elementType.let {
                    it == BINARY_EXPRESSION || it == BINARY_WITH_TYPE
                } && nextSibling.firstChildNode.elementType != CALL_EXPRESSION
            ) {
                ctx.localAdj = -1
                debug { "--inside(${nextSibling.elementType}) -> $expectedIndent" }
                ctx.exitAdjBy(p, 1)
            }
        }
    }

    private fun adjustExpectedIndentInFrontOfControlBlock(n: ASTNode, ctx: IndentContext) {
        val nextSibling = n.treeNext
        expectedIndent++
        debug { "++in_front(${nextSibling.elementType}) -> $expectedIndent" }
        ctx.exitAdjBy(nextSibling, -1)
    }

    private fun adjustExpectedIndentInFrontOfPropertyAccessor(n: ASTNode, ctx: IndentContext) {
        expectedIndent++
        debug { "++in_front(${n.treeNext.elementType}) -> $expectedIndent" }
        ctx.exitAdjBy(n.treeNext, -1)
    }

    private fun adjustExpectedIndentInFrontOfSuperTypeList(n: ASTNode, ctx: IndentContext) {
        expectedIndent++
        debug { "++in_front(${n.treeNext.elementType}) -> $expectedIndent" }
        ctx.localAdj = -1
    }

    private fun adjustExpectedIndentInsideSuperTypeList(n: ASTNode) {
        expectedIndent++
        debug { "++inside(${n.elementType}) -> $expectedIndent" }
    }

    private fun adjustExpectedIndentAfterSuperTypeList(n: ASTNode) {
        expectedIndent--
        debug { "--after(${n.elementType}) -> $expectedIndent" }
    }

    private fun adjustExpectedIndentInsideSuperTypeCall(n: ASTNode, ctx: IndentContext) {
        // Don't adjust indents for initializer lists
        if (n.treeParent?.elementType != SUPER_TYPE_LIST) {
            return
        }
        if (n.prevLeaf()?.textContains('\n') == false) {
            expectedIndent--
            debug { "--inside(${n.elementType}) -> $expectedIndent" }
            ctx.exitAdjBy(n, 1)
        }
    }

    private fun adjustExpectedIndentAfterEq(n: ASTNode, ctx: IndentContext) {
        expectedIndent++
        debug { "++after(EQ) -> $expectedIndent" }
        ctx.exitAdjBy(n.treeParent, -1)
    }

    private fun adjustExpectedIndentAfterArrow(n: ASTNode, ctx: IndentContext) {
        // Only adjust indents for arrows inside of when statements. Lambda arrows should not increase indent.
        if (n.treeParent?.elementType == WHEN_ENTRY) {
            val prevBlockLine = ctx.blockOpeningLineStack.peek() ?: -1
            if (prevBlockLine != line) {
                expectedIndent++
                debug { "++after(ARROW) -> $expectedIndent" }
                ctx.exitAdjBy(n.treeParent, -1)
            }
        }
    }

    private fun adjustExpectedIndentAfterColon(n: ASTNode, ctx: IndentContext) {
        expectedIndent++
        debug { "++after(COLON) -> $expectedIndent" }
        ctx.exitAdjBy(n.treeParent, -1)
    }

    private fun adjustExpectedIndentAfterLparInsideCondition(n: ASTNode, ctx: IndentContext) {
        expectedIndent++
        debug { "++inside(CONDITION) -> $expectedIndent" }
        ctx.exitAdjBy(n.treeParent, -1)
    }

    private fun indentStringTemplate(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
        editorConfig: EditorConfig
    ) {
        val psi = node.psi as KtStringTemplateExpression
        if (psi.isMultiLine() && psi.isFollowedByTrimIndent()) {
            val children = node.children()
            val prefixLength =
                children
                    .fold(StringBuilder()) { sb, child ->
                        when (child.elementType) {
                            LITERAL_STRING_TEMPLATE_ENTRY -> {
                                val text = child.text
                                // bail if indentation contains Tab
                                for (c in text) {
                                    if (c == '\t') {
                                        return // bail
                                    }
                                    if (!c.isWhitespace()) {
                                        break
                                    }
                                }
                                sb.append(text)
                            }
                            LONG_STRING_TEMPLATE_ENTRY -> sb.append("${'$'}{}")
                            SHORT_STRING_TEMPLATE_ENTRY -> sb.append("${'$'}")
                            else -> sb
                        }
                    }
                    .split('\n')
                    .filter(String::isNotBlank)
                    .map { it.indentLength() }
                    .min() ?: 0
            val expectedPrefixLength = expectedIndent * editorConfig.indentSize
            // TODO: uncomment, once it's clear how to indent stuff within string templates
//            if (prefixLength != expectedPrefixLength) {
//                for (child in children) {
//                    if (child.isPrecededByLFStringTemplateEntry()) {
//                        when (child.elementType) {
//                            LITERAL_STRING_TEMPLATE_ENTRY -> {
//                                val v = child.text
//                                if (v != "\n") {
//                                    val indentLength = v.indentLength()
//                                    val expectedIndentLength = indentLength - prefixLength + expectedPrefixLength
//                                    if (indentLength != expectedIndentLength) {
//                                        reindentStringTemplateEntry(
//                                            child,
//                                            autoCorrect,
//                                            emit,
//                                            indentLength,
//                                            expectedIndentLength
//                                        )
//                                    }
//                                }
//                            }
//                            LONG_STRING_TEMPLATE_ENTRY, SHORT_STRING_TEMPLATE_ENTRY -> {
//                                if (expectedPrefixLength != 0) {
//                                    preindentStringTemplateEntry(
//                                        child.firstChildNode,
//                                        autoCorrect,
//                                        emit,
//                                        expectedPrefixLength
//                                    )
//                                }
//                            }
//                        }
//                    }
//                }
//            }
            val closingQuote = children.find { it.elementType == CLOSING_QUOTE }!!
            if (closingQuote.treePrev.text == "\n") {
                // rewriting
                // (
                //     """
                // """.trimIndent()
                // )
                // to
                // (
                //     """
                //     """.trimIndent()
                // )
                if (expectedPrefixLength != 0) {
                    preindentStringTemplateEntry(closingQuote, autoCorrect, emit, expectedPrefixLength)
                }
            } else if (!closingQuote.treePrev.text.isBlank()) {
                // rewriting
                // """
                //     text
                // _""".trimIndent()
                // to
                // """
                //     text
                // _
                // """.trimIndent()
                emit(
                    closingQuote.startOffset,
                    "Missing newline before \"\"\"",
                    true
                )
                if (autoCorrect) {
                    closingQuote as LeafPsiElement
                    closingQuote.rawInsertBeforeMe(LeafPsiElement(REGULAR_STRING_PART, "\n"))
                    closingQuote.rawInsertBeforeMe(
                        LeafPsiElement(REGULAR_STRING_PART, " ".repeat(expectedPrefixLength))
                    )
                }
                debug {
                    (if (!autoCorrect) "would have " else "") +
                        "inserted newline before (closing) \"\"\""
                }
            } else { // preceded by blank LITERAL_STRING_TEMPLATE_ENTRY
                val child = closingQuote.treePrev
                val indentLength = child.text.length
                if (indentLength != expectedPrefixLength) {
                    reindentStringTemplateEntry(child, autoCorrect, emit, indentLength, expectedPrefixLength)
                }
            }
        }
    }

    private fun preindentStringTemplateEntry(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
        expectedIndentLength: Int
    ) {
        emit(
            node.startOffset,
            "Unexpected indentation (0) (should be $expectedIndentLength)",
            true
        )
        if (autoCorrect) {
            (node as LeafPsiElement).rawInsertBeforeMe(
                LeafPsiElement(REGULAR_STRING_PART, " ".repeat(expectedIndentLength))
            )
        }
        debug {
            (if (!autoCorrect) "would have " else "") +
                "changed indentation before ${node.text} to $expectedIndentLength (from 0)"
        }
    }

    private fun reindentStringTemplateEntry(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
        indentLength: Int,
        expectedIndentLength: Int
    ) {
        emit(
            node.startOffset,
            "Unexpected indentation ($indentLength) (should be $expectedIndentLength)",
            true
        )
        if (autoCorrect) {
            (node.firstChildNode as LeafPsiElement).rawReplaceWithText(
                " ".repeat(expectedIndentLength) + node.text.substring(indentLength)
            )
        }
        debug {
            (if (!autoCorrect) "would have " else "") +
                "changed indentation to $expectedIndentLength (from $indentLength)"
        }
    }

    private fun ASTNode.isPrecededByLFStringTemplateEntry() =
        treePrev?.let { it.elementType == LITERAL_STRING_TEMPLATE_ENTRY && it.text == "\n" } == true

    private fun KtStringTemplateExpression.isMultiLine(): Boolean {
        for (child in node.children()) {
            if (child.elementType == LITERAL_STRING_TEMPLATE_ENTRY) {
                val v = child.text
                if (v == "\n") {
                    return true
                }
            }
        }
        return false
    }

    private fun KtStringTemplateExpression.isFollowedByTrimIndent() =
        this.node.nextSibling { it.elementType != DOT }
            .let { it?.elementType == CALL_EXPRESSION && it.text == "trimIndent()" }

    private fun String.indentLength() =
        indexOfFirst { !it.isWhitespace() }.let { if (it == -1) length else it }

    private fun visitWhiteSpace(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
        editorConfig: EditorConfig
    ) {
        val text = node.text
        val nodeIndent = text.substringAfterLast("\n")
        val nextLeaf = node.nextLeaf()
        val nextLeafElementType = nextLeaf?.elementType
        if (nextLeafElementType == OPEN_QUOTE && nextLeaf.text == "\"\"\"" && nodeIndent.isEmpty()) {
            return // raw strings ("""") are allowed at column 0
        }
        val comment = nextLeaf?.parent({ it.psi is PsiComment }, strict = false)
        if (comment != null) {
            if (nodeIndent.isEmpty()) {
                return // comments are allowed at column 0
            }
            if (comment.textContains('\n') && comment.elementType == BLOCK_COMMENT) {
                // FIXME: while we cannot assume any kind of layout inside a block comment,
                // `/*` and `*/` can still be indented
                return
            }
        }
        // adjusting expectedIndent based on what is in front
        val adjustedExpectedIndent = expectedIndent + when {
            // IDEA quirk:
            // val v = (
            //     true
            //     )
            // instead of expected
            // val v = (
            //     true
            // )
            nextLeafElementType == RPAR &&
                node.treeParent?.elementType == PARENTHESIZED ->
                0
            // IDEA quirk:
            // class A : T<
            //     K,
            //     V
            //     > {
            // }
            // instead of expected
            // class A : T<
            //     K,
            //     V
            // > {
            // }
            nextLeafElementType == COLON ->
                1
            nextLeafElementType == GT &&
                node.treeParent?.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST } ->
                0
            nextLeafElementType in rTokenSet -> -1
            // IDEA quirk:
            // val i: Int
            //     by lazy { 1 }
            // instead of expected
            // val i: Int
            // by lazy { 1 }
            nextLeafElementType == BY_KEYWORD ->
                if (node.isPartOf(DELEGATED_SUPER_TYPE_ENTRY)) 0 else 1
            // IDEA quirk:
            // var value: DataClass =
            //     DataClass("too long line")
            //     private set
            //
            //  instead of expected:
            //  var value: DataClass =
            //      DataClass("too long line")
            //          private set
            node.nextCodeSibling()?.elementType == PROPERTY_ACCESSOR && node.treeParent.findChildByType(EQ)?.nextLeaf().isWhiteSpaceWithNewline() -> -1
            else -> 0
        }
        // indentation with incorrect characters replaced
        val normalizedNodeIndent =
            when (editorConfig.indentStyle) {
                IndentStyle.SPACE -> {
                    if ('\t' in nodeIndent) {
                        emit(
                            node.startOffset + text.length - nodeIndent.length,
                            "Unexpected tab character(s)",
                            true
                        )
                        nodeIndent.replace("\t", " ".repeat(editorConfig.tabWidth))
                    } else {
                        nodeIndent
                    }
                }
                IndentStyle.TAB -> {
                    if (' ' in nodeIndent) {
                        emit(
                            node.startOffset + text.length - nodeIndent.length,
                            "Unexpected space character(s)",
                            true
                        )
                        // First normalize the indent to spaces using the tab width.
                        val asSpaces = nodeIndent.replace("\t", " ".repeat(editorConfig.tabWidth))
                        // Then divide that space-based indent into tabs.
                        "\t".repeat(asSpaces.length / editorConfig.tabWidth)
                    } else {
                        nodeIndent
                    }
                }
            }
        val indentLength =
            when (editorConfig.indentStyle) {
                IndentStyle.SPACE -> editorConfig.indentSize
                IndentStyle.TAB -> 1
            }
        val expectedIndentLength =
            adjustedExpectedIndent * indentLength +
                // +1 space before * in `/**\n *\n */`
                if (comment?.elementType == KDOC && nextLeafElementType != KDOC_START) 1 else 0
        if (normalizedNodeIndent.length != expectedIndentLength) {
            emit(
                node.startOffset + text.length - nodeIndent.length,
                "Unexpected indentation (${normalizedNodeIndent.length}) (should be $expectedIndentLength)",
                true
            )
            debug {
                (if (!autoCorrect) "would have " else "") +
                    "changed indentation to $expectedIndentLength (from ${normalizedNodeIndent.length})"
            }
            if (autoCorrect) {
                val indent = when (editorConfig.indentStyle) {
                    IndentStyle.SPACE -> " "
                    IndentStyle.TAB -> "\t"
                }
                (node as LeafPsiElement).rawReplaceWithText(
                    text.substringBeforeLast("\n") + "\n" +
                        indent.repeat(expectedIndentLength)
                )
            }
        }
    }

    // e.g.
    // if (condition), while (condition), for (condition), ...
    private fun ASTNode.isPartOfCondition(): Boolean {
        var n = this
        while (n.treeParent?.elementType.let { it == BINARY_EXPRESSION || it == BINARY_WITH_TYPE }) {
            n = n.treeParent
        }
        return n.treeParent?.elementType == CONDITION
    }

    // e.g.
    // where T1 : SubType...,
    //       T2 : SubType...
    private fun ASTNode.isPartOfTypeConstraint() =
        isPartOf(TYPE_CONSTRAINT_LIST) || nextLeaf()?.elementType == WHERE_KEYWORD
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isRoot
import com.pinterest.ktlint.core.ast.parent
import com.pinterest.ktlint.core.ast.prevCodeSibling
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
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

class MaxLineLengthRule : Rule("max-line-length"), Rule.Modifier.Last {

    private var maxLineLength: Int = -1
    private var rangeTree = RangeTree()

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.isRoot()) {
            val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!!
            maxLineLength = editorConfig.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)!!.node
                    if (!el.isPartOf(KDoc::class) && !el.isPartOfRawMultiLineString()) {
                        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 {
                            // Allow ktlint-disable comments to exceed max line length
                            if (!el.text.startsWith("// ktlint-disable")) {
                                // if comment is the only thing on the line - fine, otherwise emit an error
                                val prevLeaf = el.prevCodeSibling()
                                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)
                }
        }
    }

    private fun ASTNode.isPartOfRawMultiLineString() =
        parent(ElementType.STRING_TEMPLATE, strict = false)
            ?.let { it.firstChildNode.text == "\"\"\"" && it.textContains('\n') } == true
}

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): RangeTree.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()
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ABSTRACT_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.ACTUAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.COMPANION_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.CONST_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.DATA_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.ENUM_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.EXPECT_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.EXTERNAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.FINAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.INFIX_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.INLINE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.INNER_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.INTERNAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.LATEINIT_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.OPEN_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.OPERATOR_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.OVERRIDE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.PRIVATE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.PROTECTED_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.PUBLIC_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.SEALED_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.SUSPEND_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.TAILREC_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.VARARG_KEYWORD
import java.util.Arrays
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtDeclarationModifierList

class ModifierOrderRule : Rule("modifier-order") {

    // subset of ElementType.MODIFIER_KEYWORDS_ARRAY (+ annotations entries)
    private val order = arrayOf(
        ANNOTATION_ENTRY,
        PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD,
        EXPECT_KEYWORD, ACTUAL_KEYWORD,
        FINAL_KEYWORD, OPEN_KEYWORD, ABSTRACT_KEYWORD, SEALED_KEYWORD, CONST_KEYWORD,
        EXTERNAL_KEYWORD,
        OVERRIDE_KEYWORD,
        LATEINIT_KEYWORD,
        TAILREC_KEYWORD,
        VARARG_KEYWORD,
        SUSPEND_KEYWORD,
        INNER_KEYWORD,
        ENUM_KEYWORD, ANNOTATION_KEYWORD,
        COMPANION_KEYWORD,
        INLINE_KEYWORD,
        INFIX_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)

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.psi is KtDeclarationModifierList) {
            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 \"${
                    squashAnnotations(sorted).joinToString(" ")
                    }\")",
                    true
                )
                if (autoCorrect) {
                    modifierArr.forEachIndexed { i, n ->
                        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 }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.nextLeaf
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

class NoBlankLineBeforeRbraceRule : Rule("no-blank-line-before-rbrace") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is PsiWhiteSpace &&
            node.textContains('\n') &&
            node.nextLeaf()?.elementType == RBRACE
        ) {
            val split = node.getText().split("\n")
            if (split.size > 2) {
                emit(
                    node.startOffset + split[0].length + split[1].length + 1,
                    "Unexpected blank line(s) before \"}\"",
                    true
                )
                if (autoCorrect) {
                    (node as LeafPsiElement).rawReplaceWithText("${split.first()}\n${split.last()}")
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLASS
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR
import com.pinterest.ktlint.core.ast.nextLeaf
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

class NoConsecutiveBlankLinesRule : Rule("no-consecutive-blank-lines") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is PsiWhiteSpace) {
            val text = node.getText()
            val lfcount = text.count { it == '\n' }
            if (lfcount < 2) {
                return
            }
            val eof = node.nextLeaf() == null
            val prevNode = node.treePrev
            val betweenClassAndPrimaryConstructor = prevNode.elementType == IDENTIFIER &&
                prevNode.treeParent.elementType == CLASS &&
                node.treeNext.elementType == PRIMARY_CONSTRUCTOR
            if (lfcount > 2 || eof || betweenClassAndPrimaryConstructor) {
                val split = text.split("\n")
                emit(node.startOffset + split[0].length + split[1].length + 2, "Needless blank line(s)", true)
                if (autoCorrect) {
                    val newText = buildString {
                        append(split.first())
                        append("\n")
                        if (!eof && !betweenClassAndPrimaryConstructor) append("\n")
                        append(split.last())
                    }
                    (node as LeafPsiElement).rawReplaceWithText(newText)
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.nextLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtObjectLiteralExpression

class NoEmptyClassBodyRule : Rule("no-empty-class-body") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == CLASS_BODY &&
            node.firstChildNode?.let { n ->
                n.elementType == LBRACE &&
                    n.nextLeaf { it.elementType != WHITE_SPACE }?.elementType == RBRACE
            } == true &&
            !node.isPartOf(KtObjectLiteralExpression::class) &&
            node.treeParent.firstChildNode.children().none { it.text == "companion" }
        ) {
            emit(node.startOffset, "Unnecessary block (\"{}\")", true)
            if (autoCorrect) {
                val prevNode = node.treePrev
                if (prevNode.elementType == WHITE_SPACE) {
                    // remove space between declaration and block
                    prevNode.treeParent.removeChild(prevNode)
                }
                // remove block
                node.treeParent.removeChild(node)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ELSE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.IF_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
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

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 (node.prevLeaf()?.elementType == ELSE_KEYWORD &&
                node.nextLeaf()?.elementType.let { it == IF_KEYWORD || it == LBRACE }
            ) {
                emit(node.startOffset + 1, "Unexpected line break after \"else\"", true)
                if (autoCorrect) {
                    (node as LeafPsiElement).rawReplaceWithText(" ")
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.EQ
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.prevCodeSibling
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.psi.psiUtil.siblings

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 == EQ) {
            val prevCodeSibling = node.prevCodeSibling()
            val hasLineBreakBeforeAssignment = prevCodeSibling
                ?.siblings()
                ?.takeWhile { it.isWhiteSpace() || it.isPartOfComment() }
                ?.any { it.isWhiteSpaceWithNewline() }
            if (hasLineBreakBeforeAssignment == true) {
                emit(node.startOffset, "Line break before assignment is not allowed", true)
                if (autoCorrect) {
                    val next = prevCodeSibling.treeNext
                    val newText = buildString {
                        append(" =")
                        if (next !is PsiWhiteSpace) append(" ")
                        append(next.text)
                    }
                    (next as? LeafPsiElement)?.rawReplaceWithText(newText)
                    (node as? LeafPsiElement)?.delete()
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.KDOC_MARKDOWN_LINK
import com.pinterest.ktlint.core.ast.ElementType.KDOC_TAG
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

class NoMultipleSpacesRule : Rule("no-multi-spaces") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is PsiWhiteSpace && !node.textContains('\n') && node.getTextLength() > 1 &&
            // allow multiple spaces in KDoc in case of KDOC_TAG for alignment, e.g.
            // @param foo      stuff
            // @param foobar   stuff2
            !(node.treePrev?.elementType == KDOC_MARKDOWN_LINK && node.treeParent?.elementType == KDOC_TAG)
        ) {
            emit(node.startOffset + 1, "Unnecessary space(s)", true)
            if (autoCorrect) {
                (node as LeafPsiElement).rawReplaceWithText(" ")
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.KDOC_TEXT
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.SEMICOLON
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isPartOfString
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevCodeLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
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.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtDoWhileExpression
import org.jetbrains.kotlin.psi.KtEnumEntry
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtLoopExpression
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType

class NoSemicolonsRule : Rule("no-semi") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == KDOC_TEXT) {
            return
        }
        if (node is LeafPsiElement &&
            node.elementType == SEMICOLON &&
            !node.isPartOfString() &&
            !node.isPartOfEnumEntry()
        ) {
            val nextLeaf = node.nextLeaf()
            val prevCodeLeaf = node.prevCodeLeaf()
            if (doesNotRequirePreSemi(nextLeaf) && doesNotRequirePostSemi(prevCodeLeaf)) {
                emit(node.startOffset, "Unnecessary semicolon", true)
                if (autoCorrect) {
                    node.treeParent.removeChild(node)
                }
            } else if (nextLeaf !is PsiWhiteSpace) {
                val prevLeaf = node.prevLeaf()
                if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { // \n;{
                    return
                }
                // todo: move to a separate rule
                emit(node.startOffset + 1, "Missing spacing after \";\"", true)
                if (autoCorrect) {
                    node.upsertWhitespaceAfterMe(" ")
                }
            }
        }
    }

    private fun doesNotRequirePreSemi(nextLeaf: ASTNode?): Boolean {
        if (nextLeaf is PsiWhiteSpace) {
            val nextNextLeaf = nextLeaf.nextLeaf {
                val psi = it.psi
                it !is PsiWhiteSpace && it !is PsiComment && psi.getStrictParentOfType<KDoc>() == null &&
                    psi.getStrictParentOfType<KtAnnotationEntry>() == null
            }
            return (
                nextNextLeaf == null || // \s+ and then eof
                    nextLeaf.textContains('\n') && nextNextLeaf.elementType != KtTokens.LBRACE
                )
        }
        return nextLeaf == null /* eof */
    }

    private fun doesNotRequirePostSemi(prevLeaf: ASTNode?): Boolean {
        if (prevLeaf?.elementType == OBJECT_KEYWORD) {
            // https://github.com/pinterest/ktlint/issues/281
            return false
        }
        val parent = prevLeaf?.treeParent?.psi
        if (parent is KtLoopExpression && parent !is KtDoWhileExpression && parent.body == null) {
            // https://github.com/pinterest/ktlint/issues/955
            return false
        }
        if (parent is KtIfExpression && parent.then == null) {
            return false
        }
        return true
    }

    private fun ASTNode.isPartOfEnumEntry(): Boolean {
        if (isPartOf(KtEnumEntry::class)) return true
        val lBrace = prevLeaf { !it.isWhiteSpace() && !it.isPartOfComment() }
            ?.takeIf { it.elementType == KtTokens.LBRACE }
            ?: return false
        val classBody = lBrace.treeParent?.psi as? KtClassBody ?: return false
        if (classBody.children.isEmpty()) return false
        return (classBody.parent as? KtClass)?.isEnum() == true
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.nextLeaf
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

class NoTrailingSpacesRule : Rule("no-trailing-spaces") {

    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) {
                val violated = checkForTrailingSpaces(lines.head(), node.startOffset, emit)
                if (violated && autoCorrect) {
                    (node as LeafPsiElement).rawReplaceWithText("\n".repeat(lines.size - 1) + lines.last())
                }
            } else if (node.nextLeaf() == 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
    ): Boolean {
        var violated = false
        var violationOffset = offset
        lines.forEach { line ->
            if (!line.isEmpty()) {
                emit(violationOffset, "Trailing space(s)", true)
                violated = true
            }
            violationOffset += line.length + 1
        }
        return violated
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.nextCodeLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

class NoUnitReturnRule : Rule("no-unit-return") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == TYPE_REFERENCE &&
            node.treeParent.elementType == FUN &&
            node.text == "Unit" &&
            node.nextCodeLeaf(skipSubtree = true)?.elementType == LBRACE
        ) {
            emit(node.startOffset, "Unnecessary \"Unit\" return type", true)
            if (autoCorrect) {
                var prevNode = node
                while (prevNode.treePrev.elementType != VALUE_PARAMETER_LIST) {
                    prevNode = prevNode.treePrev
                }
                node.treeParent.removeRange(prevNode, node.treeNext)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.BY_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE
import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isRoot
import com.pinterest.ktlint.core.ast.visit
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtPackageDirective

class NoUnusedImportsRule : Rule("no-unused-imports") {

    private val componentNRegex = Regex("^component\\d+$")

    private val operatorSet = setOf(
        // unary
        "unaryPlus", "unaryMinus", "not",
        // inc/dec
        "inc", "dec",
        // arithmetic
        "plus", "minus", "times", "div", "rem", "mod", "rangeTo",
        // in
        "contains",
        // indexed access
        "get", "set",
        // invoke
        "invoke",
        // augmented assignments
        "plusAssign", "minusAssign", "timesAssign", "divAssign", "modAssign",
        // (in)equality
        "equals",
        // comparison
        "compareTo",
        // iteration (https://github.com/shyiko/ktlint/issues/40)
        "iterator",
        // by (https://github.com/shyiko/ktlint/issues/54)
        "getValue", "setValue"
    )

    private val conditionalWhitelist = mapOf<(String) -> Boolean, (node: ASTNode) -> Boolean>(
        Pair(
            // Ignore provideDelegate if there is a `by` anywhere in the file
            { importPath -> importPath.endsWith(".provideDelegate") },
            { rootNode ->
                var hasByKeyword = false
                rootNode.visit { child ->
                    if (child.elementType == BY_KEYWORD) {
                        hasByKeyword = true
                        return@visit
                    }
                }
                hasByKeyword
            }
        )
    )

    private data class Reference(val text: String, val inDotQualifiedExpression: Boolean)
    private val ref = mutableSetOf<Reference>()
    private val parentExpressions = mutableSetOf<String>()
    private val imports = mutableSetOf<String>()
    private var packageName = ""
    private var rootNode: ASTNode? = null

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.isRoot()) {
            rootNode = node
            ref.clear() // rule can potentially be executed more than once (when formatting)
            ref.add(Reference("*", false))
            parentExpressions.clear()
            imports.clear()
            node.visit { vnode ->
                val psi = vnode.psi
                val type = vnode.elementType
                val text = vnode.text
                if (checkIfExpressionHasParentImport(text, type)) {
                    parentExpressions.add(text.substringBeforeLast("("))
                }
                if (type == KDocTokens.MARKDOWN_LINK && psi is KDocLink) {
                    val linkText = psi.getLinkText().removeBackticks()
                    ref.add(Reference(linkText.split('.').first(), false))
                } else if ((type == REFERENCE_EXPRESSION || type == OPERATION_REFERENCE) &&
                    !vnode.isPartOf(IMPORT_DIRECTIVE)
                ) {
                    ref.add(Reference(text.removeBackticks(), psi.parentDotQualifiedExpression() != null))
                } else if (type == IMPORT_DIRECTIVE) {
                    imports += (vnode.psi as KtImportDirective).importPath!!.pathStr.removeBackticks().trim()
                }
            }
            val directCalls = ref.filter { !it.inDotQualifiedExpression }.map { it.text }
            parentExpressions.forEach { parent ->
                imports.removeIf { imp ->
                    imp.endsWith(".$parent") && directCalls.none { imp.endsWith(".$it") }
                }
            }
        } else if (node.elementType == PACKAGE_DIRECTIVE) {
            val packageDirective = node.psi as KtPackageDirective
            packageName = packageDirective.qualifiedName
        } else if (node.elementType == IMPORT_DIRECTIVE) {
            val importDirective = node.psi as KtImportDirective
            val name = importDirective.importPath?.importedName?.asString()?.removeBackticks()
            val importPath = importDirective.importPath?.pathStr?.removeBackticks()!!
            if (importDirective.aliasName == null &&
                (packageName.isEmpty() || importPath.startsWith("$packageName.")) &&
                importPath.substring(packageName.length + 1).indexOf('.') == -1
            ) {
                emit(node.startOffset, "Unnecessary import", true)
                if (autoCorrect) {
                    importDirective.delete()
                }
            } else if (name != null && (!ref.map { it.text }.contains(name) || !isAValidImport(importPath)) &&
                !operatorSet.contains(name) &&
                !name.isComponentN() &&
                conditionalWhitelist
                    .filterKeys { selector -> selector(importPath) }
                    .none { (_, condition) -> condition(rootNode!!) }
            ) {
                emit(node.startOffset, "Unused import", true)
                if (autoCorrect) {
                    importDirective.delete()
                }
            }
        }
    }

    // Checks if any static method call is present in code with the parent import present in imports list
    private fun checkIfExpressionHasParentImport(text: String, type: IElementType): Boolean {
        val containsMethodCall = text.split(".").last().contains("(")
        return type == DOT_QUALIFIED_EXPRESSION && containsMethodCall && checkIfParentImportExists(text.substringBeforeLast("("))
    }

    // Contains list of all imports and checks if given import is present
    private fun checkIfParentImportExists(text: String): Boolean {
        // Only check static imports; identified if they start with a capital letter indicating a
        // class name rather than a sub-package
        if (text.isNotEmpty() && text[0] !in 'A'..'Z') {
            return false
        }

        val staticImports = imports.filter { it.endsWith(".$text") }
        staticImports.forEach { import ->
            val count = imports.count {
                it.startsWith(import.substringBefore(text))
            }
            // Parent import and static import both are present
            if (count > 1) {
                return true
            }
        }
        return false
    }

    // Check if the import being checked is present in the filtered import list
    private fun isAValidImport(importPath: String): Boolean {
        return imports.contains(importPath)
    }

    private fun String.isComponentN() = componentNRegex.matches(this)

    private fun PsiElement.parentDotQualifiedExpression(): KtDotQualifiedExpression? {
        val callOrThis = (parent as? KtCallExpression)?.takeIf { it.calleeExpression == this } ?: this
        return (callOrThis.parent as? KtDotQualifiedExpression)?.takeIf { it.selectorExpression == callOrThis }
    }

    private fun String.removeBackticks() = replace("`", "")
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.KtImportDirective

class NoWildcardImportsRule : Rule("no-wildcard-imports") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == IMPORT_DIRECTIVE) {
            val importDirective = node.psi as KtImportDirective
            val path = importDirective.importPath
            if (path != null && path.isAllUnder && !path.pathStr.startsWith("kotlinx.android.synthetic")) {
                emit(node.startOffset, "Wildcard import", false)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.column
import com.pinterest.ktlint.core.ast.isRoot
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.lineIndent
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.prevSibling
import com.pinterest.ktlint.core.ast.visit
import kotlin.math.max
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.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.psi.KtTypeArgumentList
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType

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.isRoot()) {
            val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!!
            indentSize = editorConfig.indentSize
            maxLineLength = editorConfig.maxLineLength
            return
        }
        if (indentSize <= 0) {
            return
        }
        if (node.elementType == VALUE_PARAMETER_LIST &&
            // skip when there are no parameters
            node.firstChildNode?.treeNext?.elementType != RPAR &&
            // skip lambda parameters
            node.treeParent?.elementType != 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.column - 1 + node.textLength) > maxLineLength
            if (putParametersOnSeparateLines) {
                // IDEA quirk:
                // fun <
                //     T,
                //     R> test(
                //     param1: T
                //     param2: R
                // )
                // instead of
                // fun <
                //     T,
                //     R> test(
                //         param1: T
                //         param2: R
                //     )
                val adjustedIndent = if (node.hasTypeParameterListInFront()) indentSize else 0

                // aiming for
                // ... LPAR
                // <line indent + indentSize> VALUE_PARAMETER...
                // <line indent> RPAR
                val lineIndent = node.lineIndent()
                val indent = "\n" + lineIndent.substring(0, (lineIndent.length - adjustedIndent).coerceAtLeast(0))
                val paramIndent = indent + " ".repeat(indentSize)
                nextChild@ for (child in node.children()) {
                    when (child.elementType) {
                        LPAR -> {
                            val prevLeaf = child.prevLeaf()
                            if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    prevLeaf.delete()
                                }
                            }
                        }
                        VALUE_PARAMETER,
                        RPAR -> {
                            var paramInnerIndentAdjustment = 0
                            val prevLeaf = child.prevLeaf()
                            val intendedIndent = if (child.elementType == VALUE_PARAMETER) {
                                paramIndent
                            } else {
                                indent
                            }
                            if (prevLeaf is PsiWhiteSpace) {
                                val spacing = prevLeaf.getText()
                                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.getTextLength()
                                    (prevLeaf as LeafPsiElement).rawReplaceWithText(adjustedIndent)
                                }
                            } else {
                                emit(child.startOffset, errorMessage(child), true)
                                if (autoCorrect) {
                                    paramInnerIndentAdjustment = intendedIndent.length - child.column
                                    node.addChild(PsiWhiteSpaceImpl(intendedIndent), child)
                                }
                            }
                            if (paramInnerIndentAdjustment != 0 && child.elementType == VALUE_PARAMETER) {
                                child.visit { n ->
                                    if (n.elementType == WHITE_SPACE && n.textContains('\n')) {
                                        val split = n.text.split("\n")
                                        (n as LeafElement).rawReplaceWithText(
                                            split.joinToString("\n") {
                                                if (paramInnerIndentAdjustment > 0) {
                                                    it + " ".repeat(paramInnerIndentAdjustment)
                                                } else {
                                                    it.substring(0, max(it.length + paramInnerIndentAdjustment, 0))
                                                }
                                            }
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private fun errorMessage(node: ASTNode) =
        when (node.elementType) {
            LPAR -> """Unnecessary newline before "(""""
            VALUE_PARAMETER ->
                "Parameter should be on a separate line (unless all parameters can fit a single line)"
            RPAR -> """Missing newline before ")""""
            else -> throw UnsupportedOperationException()
        }

    private fun ASTNode.hasTypeParameterListInFront(): Boolean {
        val parent = this.treeParent
        val typeParameterList = if (parent.elementType == PRIMARY_CONSTRUCTOR) {
            parent.prevSibling { it.elementType == TYPE_PARAMETER_LIST }
        } else {
            parent.children().firstOrNull { it.elementType == TYPE_PARAMETER_LIST }
        }
        val typeListNode = typeParameterList
            ?: parent.psi.collectDescendantsOfType<KtTypeArgumentList>().firstOrNull()?.node
            ?: return false
        return typeListNode.children().any { it.isWhiteSpaceWithNewline() }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isPartOfString
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
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.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtConstructor
import org.jetbrains.kotlin.psi.KtTypeConstraint
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
    ) {
        if (node is LeafPsiElement && node.textMatches(":") && !node.isPartOfString() && !node.isPartOfComment()) {
            if (node.isPartOf(ANNOTATION) || node.isPartOf(ANNOTATION_ENTRY)) {
                // todo: enforce "no spacing"
                return
            }
            val removeSpacingBefore =
                node.parent !is KtClassOrObject &&
                    node.parent !is KtConstructor<*> && // constructor : this/super
                    node.parent !is KtTypeConstraint && // where T : S
                    node.parent?.parent !is KtTypeParameterList
            val prevLeaf = node.prevLeaf()
            if (prevLeaf != null &&
                prevLeaf.isWhiteSpaceWithNewline() &&
                // FIXME: relocate : so that it would be in front of comment
                // (see SpacingAroundColonRuleTest.testFormatEOF & ChainWrappingRule)
                prevLeaf.prevLeaf()?.isPartOfComment() != true
            ) {
                emit(prevLeaf.startOffset, "Unexpected newline before \":\"", true)
                val text = prevLeaf.text
                if (autoCorrect) {
                    if (removeSpacingBefore) {
                        prevLeaf.treeParent.removeChild(prevLeaf)
                    } else {
                        (prevLeaf as LeafPsiElement).rawReplaceWithText(" ")
                    }
                    node.upsertWhitespaceAfterMe(text)
                }
            }
            if (node.prevSibling is PsiWhiteSpace && removeSpacingBefore) {
                emit(node.startOffset, "Unexpected spacing before \":\"", true)
                if (autoCorrect) {
                    node.prevSibling.node.treeParent.removeChild(node.prevSibling.node)
                }
            }
            val missingSpacingBefore =
                node.prevSibling !is PsiWhiteSpace &&
                    (
                        node.parent is KtClassOrObject || node.parent is KtConstructor<*> ||
                            node.parent is KtTypeConstraint || node.parent.parent is KtTypeParameterList
                        )
            val missingSpacingAfter = node.nextSibling !is PsiWhiteSpace
            when {
                missingSpacingBefore && missingSpacingAfter -> {
                    emit(node.startOffset, "Missing spacing around \":\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
                missingSpacingBefore -> {
                    emit(node.startOffset, "Missing spacing before \":\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                    }
                }
                missingSpacingAfter -> {
                    emit(node.startOffset + 1, "Missing spacing after \":\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.GT
import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.isPartOfString
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.prevCodeLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
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.tree.TokenSet

class SpacingAroundCommaRule : Rule("comma-spacing") {

    private val rTokenSet = TokenSet.create(RPAR, RBRACKET, GT)

    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 = node.prevLeaf()
            if (prevLeaf is PsiWhiteSpace) {
                emit(prevLeaf.startOffset, "Unexpected spacing before \"${node.text}\"", true)
                if (autoCorrect) {
                    val isPrecededByComment = prevLeaf.prevLeaf { it !is PsiWhiteSpace } is PsiComment
                    if (isPrecededByComment && prevLeaf.isWhiteSpaceWithNewline()) {
                        // If comma is on new line and preceded by a comment, it should be moved before this comment
                        // https://github.com/pinterest/ktlint/issues/367
                        val previousStatement = node.prevCodeLeaf()!!
                        previousStatement.treeParent.addChild(node.clone(), previousStatement.nextSibling { true })
                        val nextLeaf = node.nextLeaf()
                        if (nextLeaf is PsiWhiteSpace) {
                            nextLeaf.treeParent.removeChild(nextLeaf)
                        }
                        node.treeParent.removeChild(node)
                    } else {
                        prevLeaf.treeParent.removeChild(prevLeaf)
                    }
                }
            }
            val nextLeaf = node.nextLeaf()
            if (nextLeaf !is PsiWhiteSpace && nextLeaf?.elementType !in rTokenSet) {
                emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true)
                if (autoCorrect) {
                    node.upsertWhitespaceAfterMe(" ")
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.AT
import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
import com.pinterest.ktlint.core.ast.ElementType.COLONCOLON
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EXCLEXCL
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.LBRACE
import com.pinterest.ktlint.core.ast.ElementType.LBRACKET
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
import com.pinterest.ktlint.core.ast.ElementType.SEMICOLON
import com.pinterest.ktlint.core.ast.isPartOfString
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
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.TreeElement
import org.jetbrains.kotlin.lexer.KtTokens
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
    ) {
        if (node is LeafPsiElement && !node.isPartOfString()) {
            val prevLeaf = node.prevLeaf()
            val nextLeaf = node.nextLeaf()
            val spacingBefore: Boolean
            val spacingAfter: Boolean
            if (node.elementType == LBRACE) {
                spacingBefore =
                    prevLeaf is PsiWhiteSpace ||
                    prevLeaf?.elementType == AT ||
                    (
                        prevLeaf?.elementType == LPAR &&
                            (node.parent is KtLambdaExpression || node.parent.parent is KtLambdaExpression)
                        )
                spacingAfter = nextLeaf is PsiWhiteSpace || nextLeaf?.elementType == RBRACE
                if (prevLeaf is PsiWhiteSpace &&
                    !prevLeaf.textContains('\n') &&
                    prevLeaf.prevLeaf()?.let {
                        it.elementType == LPAR || it.elementType == AT
                    } == true
                ) {
                    emit(node.startOffset, "Unexpected space before \"${node.text}\"", true)
                    if (autoCorrect) {
                        prevLeaf.node.treeParent.removeChild(prevLeaf.node)
                    }
                }
                if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n') &&
                    (
                        prevLeaf.prevLeaf()?.let { it.elementType == RPAR || KtTokens.KEYWORDS.contains(it.elementType) } == true ||
                            node.treeParent.elementType == CLASS_BODY ||
                            (prevLeaf.treeParent.elementType == FUN && prevLeaf.treeNext.elementType != LAMBDA_EXPRESSION)
                        ) // allow newline for lambda return type
                ) {
                    emit(node.startOffset, "Unexpected newline before \"${node.text}\"", true)
                    if (autoCorrect) {
                        val eolCommentExists = prevLeaf.prevLeaf()?.let {
                            it is PsiComment && it.elementType == EOL_COMMENT
                        } ?: false
                        if (eolCommentExists) {
                            val commentLeaf = prevLeaf.prevLeaf()!!
                            if (commentLeaf.prevLeaf() is PsiWhiteSpace) {
                                (commentLeaf.prevLeaf() as LeafPsiElement).rawRemove()
                            }
                            (node.treeParent.treeParent as TreeElement).removeChild(commentLeaf)
                            (node.treeParent as TreeElement).addChild(commentLeaf, node.treeNext)
                            node.upsertWhitespaceAfterMe(" ")
                        }
                        (prevLeaf as LeafPsiElement).rawReplaceWithText(" ")
                    }
                }
            } else if (node.elementType == RBRACE) {
                spacingBefore = prevLeaf is PsiWhiteSpace || prevLeaf?.elementType == LBRACE
                spacingAfter = nextLeaf == null || nextLeaf is PsiWhiteSpace || shouldNotToBeSeparatedBySpace(nextLeaf)
                if (nextLeaf is PsiWhiteSpace && !nextLeaf.textContains('\n') &&
                    shouldNotToBeSeparatedBySpace(nextLeaf.nextLeaf())
                ) {
                    emit(node.startOffset, "Unexpected space after \"${node.text}\"", true)
                    if (autoCorrect) {
                        nextLeaf.node.treeParent.removeChild(nextLeaf.node)
                    }
                }
            } else {
                return
            }
            when {
                !spacingBefore && !spacingAfter -> {
                    emit(node.startOffset, "Missing spacing around \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
                !spacingBefore -> {
                    emit(node.startOffset, "Missing spacing before \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                    }
                }
                !spacingAfter -> {
                    emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
            }
        }
    }

    fun shouldNotToBeSeparatedBySpace(leaf: ASTNode?): Boolean {
        val nextElementType = leaf?.elementType
        return (
            nextElementType == DOT ||
                nextElementType == COMMA ||
                nextElementType == RBRACKET ||
                nextElementType == RPAR ||
                nextElementType == SEMICOLON ||
                nextElementType == SAFE_ACCESS ||
                nextElementType == EXCLEXCL ||
                nextElementType == LBRACKET ||
                nextElementType == LPAR ||
                nextElementType == COLONCOLON
            )
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isPartOfString
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
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

class SpacingAroundDotRule : Rule("dot-spacing") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node is LeafPsiElement && node.textMatches(".") && !node.isPartOfString() && !node.isPartOfComment()) {
            val prevLeaf = node.prevLeaf()
            if (prevLeaf is PsiWhiteSpace && !prevLeaf.textContains('\n')) {
                emit(prevLeaf.startOffset, "Unexpected spacing before \"${node.text}\"", true)
                if (autoCorrect) {
                    prevLeaf.node.treeParent.removeChild(prevLeaf.node)
                }
            }
            val nextLeaf = node.nextLeaf()
            if (nextLeaf is PsiWhiteSpace) {
                emit(nextLeaf.startOffset, "Unexpected spacing after \"${node.text}\"", true)
                if (autoCorrect) {
                    nextLeaf.node.treeParent.removeChild(nextLeaf.node)
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CATCH_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.DO_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.ELSE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.FINALLY_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.FOR_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.GET_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.IF_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.RBRACE
import com.pinterest.ktlint.core.ast.ElementType.SET_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.TRY_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHEN_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHILE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
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.LeafElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet.create
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtWhenEntry

class SpacingAroundKeywordRule : Rule("keyword-spacing") {

    private val noLFBeforeSet = create(ELSE_KEYWORD, CATCH_KEYWORD, FINALLY_KEYWORD)
    private val tokenSet = create(
        FOR_KEYWORD, IF_KEYWORD, ELSE_KEYWORD, WHILE_KEYWORD, DO_KEYWORD,
        TRY_KEYWORD, CATCH_KEYWORD, FINALLY_KEYWORD, WHEN_KEYWORD
    )

    private val keywordsWithoutSpaces = create(GET_KEYWORD, SET_KEYWORD)

    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.parent !is KDocName && node.nextLeaf() !is PsiWhiteSpace) {
                emit(node.startOffset + node.text.length, "Missing spacing after \"${node.text}\"", true)
                if (autoCorrect) {
                    node.upsertWhitespaceAfterMe(" ")
                }
            } else if (keywordsWithoutSpaces.contains(node.elementType) && node.nextLeaf() is PsiWhiteSpace) {
                val parent = node.parent
                val nextLeaf = node.nextLeaf()
                if (parent is KtPropertyAccessor && parent.hasBody() && nextLeaf != null) {
                    emit(node.startOffset, "Unexpected spacing after \"${node.text}\"", true)
                    if (autoCorrect) {
                        nextLeaf.treeParent.removeChild(nextLeaf)
                    }
                }
            }
            if (noLFBeforeSet.contains(node.elementType)) {
                val prevLeaf = node.prevLeaf()
                val isElseKeyword = node.elementType == ELSE_KEYWORD
                if (
                    prevLeaf?.elementType == WHITE_SPACE &&
                    prevLeaf.textContains('\n') &&
                    (!isElseKeyword || node.parent !is KtWhenEntry)
                ) {
                    val rBrace = prevLeaf.prevLeaf()?.takeIf { it.elementType == RBRACE }
                    val parentOfRBrace = rBrace?.treeParent
                    if (
                        parentOfRBrace is KtBlockExpression &&
                        (!isElseKeyword || parentOfRBrace.treeParent?.treeParent == node.treeParent)
                    ) {
                        emit(node.startOffset, "Unexpected newline before \"${node.text}\"", true)
                        if (autoCorrect) {
                            (prevLeaf as LeafElement).rawReplaceWithText(" ")
                        }
                    }
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANDAND
import com.pinterest.ktlint.core.ast.ElementType.ARROW
import com.pinterest.ktlint.core.ast.ElementType.DIV
import com.pinterest.ktlint.core.ast.ElementType.DIVEQ
import com.pinterest.ktlint.core.ast.ElementType.ELVIS
import com.pinterest.ktlint.core.ast.ElementType.EQ
import com.pinterest.ktlint.core.ast.ElementType.EQEQ
import com.pinterest.ktlint.core.ast.ElementType.EQEQEQ
import com.pinterest.ktlint.core.ast.ElementType.EXCLEQ
import com.pinterest.ktlint.core.ast.ElementType.EXCLEQEQEQ
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.GT
import com.pinterest.ktlint.core.ast.ElementType.GTEQ
import com.pinterest.ktlint.core.ast.ElementType.LT
import com.pinterest.ktlint.core.ast.ElementType.LTEQ
import com.pinterest.ktlint.core.ast.ElementType.MINUS
import com.pinterest.ktlint.core.ast.ElementType.MINUSEQ
import com.pinterest.ktlint.core.ast.ElementType.MUL
import com.pinterest.ktlint.core.ast.ElementType.MULTEQ
import com.pinterest.ktlint.core.ast.ElementType.OROR
import com.pinterest.ktlint.core.ast.ElementType.PERC
import com.pinterest.ktlint.core.ast.ElementType.PERCEQ
import com.pinterest.ktlint.core.ast.ElementType.PLUS
import com.pinterest.ktlint.core.ast.ElementType.PLUSEQ
import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.parent
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
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.LeafElement
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.KtPrefixExpression
import org.jetbrains.kotlin.psi.KtSuperExpression
import org.jetbrains.kotlin.psi.KtTypeArgumentList

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
    ) {
        if (tokenSet.contains(node.elementType) &&
            node is LeafElement &&
            !node.isPartOf(KtPrefixExpression::class) && // not unary
            !node.isPartOf(KtTypeArgumentList::class) && // C<T>
            !(node.elementType == MUL && node.treeParent.elementType == VALUE_ARGUMENT) && // fn(*array)
            !node.isPartOf(KtImportDirective::class) && // import *
            !node.isPartOf(KtSuperExpression::class) // super<T>
        ) {
            if (node.elementType == LT || node.elementType == GT) {
                val list = node.parent(TYPE_PARAMETER_LIST)
                if (list != null) {
                    if (node.elementType == LT) {
                        // ensure no space after < in <T>
                        val nextLeaf = node.nextLeaf()
                        if (nextLeaf?.elementType == WHITE_SPACE && !nextLeaf.textContains('\n')) {
                            emit(node.startOffset + 1, "Unexpected spacing after \"${node.text}\"", true)
                            if (autoCorrect) {
                                nextLeaf.treeParent.removeChild(nextLeaf)
                            }
                        }
                    } else {
                        // ensure no space before > in <T>
                        val prevLeaf = node.prevLeaf()
                        if (prevLeaf?.elementType == WHITE_SPACE && !prevLeaf.textContains('\n')) {
                            emit(prevLeaf.startOffset, "Unexpected spacing before \"${node.text}\"", true)
                            if (autoCorrect) {
                                prevLeaf.treeParent.removeChild(prevLeaf)
                            }
                        }
                    }
                    if (node.parent(TYPE_PARAMETER_LIST)?.treeParent?.elementType != FUN) {
                        return // FIXME: only `fun <T>fn(): T {}` is checked
                    }
                }
            }
            val spacingBefore = node.prevLeaf() is PsiWhiteSpace || node.elementType == GT
            val spacingAfter = node.nextLeaf() is PsiWhiteSpace || node.elementType == LT
            when {
                !spacingBefore && !spacingAfter -> {
                    emit(node.startOffset, "Missing spacing around \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
                !spacingBefore -> {
                    emit(node.startOffset, "Missing spacing before \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceBeforeMe(" ")
                    }
                }
                !spacingAfter -> {
                    emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true)
                    if (autoCorrect) {
                        node.upsertWhitespaceAfterMe(" ")
                    }
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_TYPE
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.KDOC_START
import com.pinterest.ktlint.core.ast.ElementType.LPAR
import com.pinterest.ktlint.core.ast.ElementType.RPAR
import com.pinterest.ktlint.core.ast.ElementType.SUPER_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace

/**
 * Ensures there are no extra spaces around parentheses.
 *
 * See https://kotlinlang.org/docs/reference/coding-conventions.html#horizontal-whitespace
 */
class SpacingAroundParensRule : Rule("paren-spacing") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == LPAR || node.elementType == RPAR) {
            val prevLeaf = node.prevLeaf()
            val nextLeaf = node.nextLeaf()
            val spacingBefore = if (node.elementType == LPAR) {
                prevLeaf is PsiWhiteSpace && !prevLeaf.textContains('\n') &&
                    (
                        prevLeaf.prevLeaf()?.elementType == IDENTIFIER &&
                            // val foo: @Composable () -> Unit
                            node.treeParent?.treeParent?.elementType != FUNCTION_TYPE ||
                            // Super keyword needs special-casing
                            prevLeaf.prevLeaf()?.elementType == SUPER_KEYWORD
                        ) &&
                    (
                        node.treeParent?.elementType == VALUE_PARAMETER_LIST ||
                            node.treeParent?.elementType == VALUE_ARGUMENT_LIST
                        )
            } else {
                prevLeaf is PsiWhiteSpace && !prevLeaf.textContains('\n') &&
                    prevLeaf.prevLeaf()?.elementType != LPAR
            }
            val spacingAfter = if (node.elementType == LPAR) {
                nextLeaf is PsiWhiteSpace &&
                    (!nextLeaf.textContains('\n') || nextLeaf.nextLeaf()?.elementType == RPAR) &&
                    !nextLeaf.isNextLeafAComment()
            } else {
                nextLeaf is PsiWhiteSpace && !nextLeaf.textContains('\n') &&
                    nextLeaf.nextLeaf()?.elementType == RPAR
            }
            when {
                spacingBefore && spacingAfter -> {
                    emit(node.startOffset, "Unexpected spacing around \"${node.text}\"", true)
                    if (autoCorrect) {
                        prevLeaf!!.treeParent.removeChild(prevLeaf)
                        nextLeaf!!.treeParent.removeChild(nextLeaf)
                    }
                }
                spacingBefore -> {
                    emit(prevLeaf!!.startOffset, "Unexpected spacing before \"${node.text}\"", true)
                    if (autoCorrect) {
                        prevLeaf.treeParent.removeChild(prevLeaf)
                    }
                }
                spacingAfter -> {
                    emit(node.startOffset + 1, "Unexpected spacing after \"${node.text}\"", true)
                    if (autoCorrect) {
                        nextLeaf!!.treeParent.removeChild(nextLeaf)
                    }
                }
            }
        }
    }

    private fun ASTNode.isNextLeafAComment(): Boolean {
        val commentTypes = setOf(EOL_COMMENT, BLOCK_COMMENT, KDOC_START)
        return nextLeaf()?.elementType in commentTypes
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.RANGE
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace

class SpacingAroundRangeOperatorRule : Rule("range-spacing") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == RANGE) {
            val prevLeaf = node.prevLeaf()
            val nextLeaf = node.nextLeaf()
            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)
                    }
                }
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class StandardRuleSetProvider : RuleSetProvider {

    // Note: some of these rules may be disabled by default. See the default .editorconfig.
    override fun get(): RuleSet = RuleSet(
        "standard",
        ChainWrappingRule(),
        CommentSpacingRule(),
        FilenameRule(),
        FinalNewlineRule(),
        ImportOrderingRule(),
        IndentationRule(),
        MaxLineLengthRule(),
        ModifierOrderRule(),
        NoBlankLineBeforeRbraceRule(),
        NoConsecutiveBlankLinesRule(),
        NoEmptyClassBodyRule(),
        NoLineBreakAfterElseRule(),
        NoLineBreakBeforeAssignmentRule(),
        NoMultipleSpacesRule(),
        NoSemicolonsRule(),
        NoTrailingSpacesRule(),
        NoUnitReturnRule(),
        NoUnusedImportsRule(),
        NoWildcardImportsRule(),
        ParameterListWrappingRule(),
        SpacingAroundColonRule(),
        SpacingAroundCommaRule(),
        SpacingAroundCurlyRule(),
        SpacingAroundDotRule(),
        SpacingAroundKeywordRule(),
        SpacingAroundOperatorsRule(),
        SpacingAroundParensRule(),
        SpacingAroundRangeOperatorRule(),
        StringTemplateRule()
    )
}

package com.pinterest.ktlint.ruleset.standard

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE
import com.pinterest.ktlint.core.ast.ElementType.DOT
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.SUPER_EXPRESSION
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.psi.KtBlockStringTemplateEntry
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtThisExpression

class StringTemplateRule : Rule("string-template") {

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        val elementType = node.elementType
        // code below is commented out because (setting aside potentially dangerous replaceChild part)
        // `val v: String = "$elementType"` would be rewritten to `val v: String = elementType.toString()` and it's not
        // immediately clear which is better
/*
        if (elementType === SHORT_STRING_TEMPLATE_ENTRY || elementType == LONG_STRING_TEMPLATE_ENTRY) {
            if (node.treePrev.elementType == OPEN_QUOTE && node.treeNext.elementType == CLOSING_QUOTE) {
                emit(node.treePrev.startOffset, "Redundant string template", true)
                val entryStart = node.psi.firstChild
                node.treeParent.treeParent.replaceChild(node.treeParent, entryStart.nextSibling.node)
            }
*/
        if (elementType == LONG_STRING_TEMPLATE_ENTRY) {
            var entryExpression = (node.psi as? KtBlockStringTemplateEntry)?.expression
            val entryStart = node.firstChildNode
            val dotQualifiedExpression = entryStart.treeNext
            if (dotQualifiedExpression?.elementType == DOT_QUALIFIED_EXPRESSION) {
                val callExpression = dotQualifiedExpression.lastChildNode
                val dot = callExpression.treePrev
                if (dot?.elementType == DOT &&
                    callExpression.text == "toString()" &&
                    dotQualifiedExpression.firstChildNode?.elementType != SUPER_EXPRESSION
                ) {
                    emit(dot.startOffset, "Redundant \"toString()\" call in string template", true)
                    if (autoCorrect) {
                        entryExpression = (entryExpression as? KtDotQualifiedExpression)?.receiverExpression
                        node.removeChild(dot)
                        node.removeChild(callExpression)
                    }
                }
            }
            if (node.text.startsWith("${'$'}{") &&
                node.text.let { it.substring(2, it.length - 1) }.all { it.isPartOfIdentifier() } &&
                (entryExpression is KtNameReferenceExpression || entryExpression is KtThisExpression) &&
                node.treeNext.let { nextSibling ->
                    nextSibling.elementType == CLOSING_QUOTE ||
                        (
                            nextSibling.elementType == LITERAL_STRING_TEMPLATE_ENTRY &&
                                !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.firstChildNode as LeafPsiElement).rawReplaceWithText("$") // entry start
                    (node.lastChildNode as LeafPsiElement).rawReplaceWithText("") // entry end
                }
            }
        }
    }

    private fun Char.isPartOfIdentifier() = this == '_' || this.isLetterOrDigit()
}

package com.pinterest.ktlint.ruleset.standard

internal fun <T> List<T>.head() = this.subList(0, this.size - 1)
internal fun <T> List<T>.tail() = this.subList(1, this.size)

package com.pinterest.ktlint.reporter.plain.internal

/**
 * Stripped down version of https://github.com/ziggy42/kolor (ziggy42/kolor#6).
 */
fun String.color(foreground: Color) = "\u001B[${foreground.code}m$this\u001B[0m"

enum class Color(val code: Int) {
    BLACK(30),
    RED(31),
    GREEN(32),
    YELLOW(33),
    BLUE(34),
    MAGENTA(35),
    CYAN(36),
    LIGHT_GRAY(37),
    DARK_GRAY(90),
    LIGHT_RED(91),
    LIGHT_GREEN(92),
    LIGHT_YELLOW(93),
    LIGHT_BLUE(94),
    LIGHT_MAGENTA(95),
    LIGHT_CYAN(96),
    WHITE(97)
}

package com.pinterest.ktlint.ruleset.standard.internal.importordering

internal const val BLANK_LINE_CHAR = "|"
internal const val WILDCARD_CHAR = "*"
internal const val ALIAS_CHAR = "^"

/**
 * Adapted from https://github.com/JetBrains/intellij-kotlin/blob/73b5a484198f02518c9ece2fb453d27cead680fb/idea/src/org/jetbrains/kotlin/idea/formatter/KotlinPackageEntryTableAccessor.kt#L27-L43
 */
internal fun parseImportsLayout(importsLayout: String): List<PatternEntry> {
    val importsList = importsLayout.split(",").onEach { it.trim() }

    if (importsList.first() == BLANK_LINE_CHAR || importsList.last() == BLANK_LINE_CHAR) {
        throw IllegalArgumentException("Blank lines are not supported in the beginning or end of import list")
    }

    if (WILDCARD_CHAR !in importsList) {
        throw IllegalArgumentException("<all other imports> symbol (\"*\") must be present in the custom imports layout")
    }

    return importsList.map {
        var import = it
        if (import == BLANK_LINE_CHAR) {
            return@map PatternEntry.BLANK_LINE_ENTRY
        } else {
            var hasAlias = false
            var withSubpackages = false
            if (import.startsWith(ALIAS_CHAR)) {
                import = import.substring(1).trim()
                hasAlias = true
            }
            if (import.endsWith(WILDCARD_CHAR + WILDCARD_CHAR)) { // java.**
                import = import.substringBeforeLast(WILDCARD_CHAR)
                withSubpackages = true
            }
            return@map if (import == WILDCARD_CHAR) { // *
                PatternEntry.ALL_OTHER_IMPORTS_ENTRY
            } else if (import.isEmpty() && hasAlias) { // ^
                PatternEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY
            } else {
                PatternEntry(import, withSubpackages, hasAlias)
            }
        }
    }
}

package com.pinterest.ktlint.ruleset.standard.internal.importordering

import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.resolve.ImportPath

/**
 * Sorts the imports according to the order specified in [patterns] + alphabetically.
 *
 * Adopted from https://github.com/JetBrains/kotlin/blob/a270ee094c4d7b9520e0898a242bb6ce4dfcad7b/idea/src/org/jetbrains/kotlin/idea/util/ImportPathComparator.kt#L15
 */
internal class ImportSorter(
    val patterns: List<PatternEntry>
) : Comparator<KtImportDirective> {

    override fun compare(import1: KtImportDirective, import2: KtImportDirective): Int {
        val importPath1 = import1.importPath!!
        val importPath2 = import2.importPath!!

        return compareValuesBy(
            importPath1,
            importPath2,
            { import -> findImportIndex(import) },
            { import -> import.toString() }
        )
    }

    fun findImportIndex(path: ImportPath): Int {
        var bestIndex: Int = -1
        var bestEntryMatch: PatternEntry? = null
        var allOtherAliasIndex = -1
        var allOtherIndex = -1

        for ((index, entry) in patterns.withIndex()) {
            if (entry == PatternEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY) {
                allOtherAliasIndex = index
            }
            if (entry == PatternEntry.ALL_OTHER_IMPORTS_ENTRY) {
                allOtherIndex = index
            }
            if (entry.isBetterMatchForPackageThan(bestEntryMatch, path)) {
                bestEntryMatch = entry
                bestIndex = index
            }
        }

        if (bestIndex == -1 && path.hasAlias() && allOtherAliasIndex == -1 && allOtherIndex != -1) {
            // if no layout for alias imports specified, put them among all others
            bestIndex = allOtherIndex
        }
        return bestIndex
    }
}

package com.pinterest.ktlint.ruleset.standard.internal.importordering

import org.jetbrains.kotlin.resolve.ImportPath

/**
 * Represents an entry in the imports layout pattern. Contains matching logic for imports.
 *
 * Adopted from https://github.com/JetBrains/kotlin/blob/ffdab473e28d0d872136b910eb2e0f4beea2e19c/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntry.kt#L10
 */
internal class PatternEntry(
    packageName: String,
    val withSubpackages: Boolean,
    val hasAlias: Boolean
) {

    private val packageName = packageName.removeSuffix(".*")

    private fun matchesPackageName(otherPackageName: String): Boolean {
        if (this == ALL_OTHER_IMPORTS_ENTRY || this == ALL_OTHER_ALIAS_IMPORTS_ENTRY) return true
        if (this == BLANK_LINE_ENTRY) return false

        if (otherPackageName.startsWith(packageName)) {
            if (otherPackageName.length == packageName.length) return true
            if (withSubpackages) {
                if (otherPackageName[packageName.length] == '.') return true
            }
        }
        return false
    }

    fun isBetterMatchForPackageThan(entry: PatternEntry?, import: ImportPath): Boolean {
        if (hasAlias != import.hasAlias() || !matchesPackageName(import.pathStr)) return false
        if (entry == null) return true

        if (entry.hasAlias != hasAlias) return false
        // Any matched package is better than ALL_OTHER_IMPORTS_ENTRY
        if (this == ALL_OTHER_IMPORTS_ENTRY) return false
        if (entry == ALL_OTHER_IMPORTS_ENTRY) return true

        if (entry.withSubpackages != withSubpackages) return !withSubpackages

        return entry.packageName.count { it == '.' } < packageName.count { it == '.' }
    }

    override fun toString(): String = when (this) {
        ALL_OTHER_IMPORTS_ENTRY -> WILDCARD_CHAR
        ALL_OTHER_ALIAS_IMPORTS_ENTRY -> ALIAS_CHAR
        else -> "$packageName.$WILDCARD_CHAR" + if (withSubpackages) WILDCARD_CHAR else ""
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as PatternEntry

        if (withSubpackages != other.withSubpackages) return false
        if (hasAlias != other.hasAlias) return false
        if (packageName != other.packageName) return false

        return true
    }

    override fun hashCode(): Int {
        var result = withSubpackages.hashCode()
        result = 31 * result + hasAlias.hashCode()
        result = 31 * result + packageName.hashCode()
        return result
    }

    companion object {
        val BLANK_LINE_ENTRY = PatternEntry(BLANK_LINE_CHAR, withSubpackages = true, hasAlias = false)
        val ALL_OTHER_IMPORTS_ENTRY = PatternEntry(WILDCARD_CHAR, withSubpackages = true, hasAlias = false)
        val ALL_OTHER_ALIAS_IMPORTS_ENTRY = PatternEntry(ALIAS_CHAR, withSubpackages = true, hasAlias = true)
    }
}