diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml
index 7574e161edb8..d07a2c1613a9 100644
--- a/detekt-core/src/main/resources/default-detekt-config.yml
+++ b/detekt-core/src/main/resources/default-detekt-config.yml
@@ -597,6 +597,9 @@ style:
active: true
MultilineLambdaItParameter:
active: false
+ MultilineRawStringIndentation:
+ active: false
+ indentSize: 4
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentation.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentation.kt
new file mode 100644
index 000000000000..cea3b690c538
--- /dev/null
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentation.kt
@@ -0,0 +1,185 @@
+package io.gitlab.arturbosch.detekt.rules.style
+
+import io.github.detekt.psi.getLineAndColumnInPsiFile
+import io.github.detekt.psi.toFilePath
+import io.gitlab.arturbosch.detekt.api.CodeSmell
+import io.gitlab.arturbosch.detekt.api.Config
+import io.gitlab.arturbosch.detekt.api.Debt
+import io.gitlab.arturbosch.detekt.api.Entity
+import io.gitlab.arturbosch.detekt.api.Issue
+import io.gitlab.arturbosch.detekt.api.Location
+import io.gitlab.arturbosch.detekt.api.Rule
+import io.gitlab.arturbosch.detekt.api.Severity
+import io.gitlab.arturbosch.detekt.api.SourceLocation
+import io.gitlab.arturbosch.detekt.api.TextLocation
+import io.gitlab.arturbosch.detekt.api.config
+import io.gitlab.arturbosch.detekt.api.internal.Configuration
+import org.jetbrains.kotlin.com.intellij.psi.PsiFile
+import org.jetbrains.kotlin.psi.KtElement
+import org.jetbrains.kotlin.psi.KtStringTemplateExpression
+
+/**
+ * This rule ensure that the raw strings have a consistent indentation.
+ *
+ * The baseIndentation is the indentation that has the line where the raw string started. The content of the
+ * raw string should have baseIndent plus one identation extra. And the closing raw string (`"""`) should have
+ * baseIndentation.
+ *
+ *
+ * val a = """
+ * Hello World!
+ * How are you?
+ * """.trimMargin()
+ *
+ * val a = """
+ * Hello World!
+ * How are you?
+ * """.trimMargin()
+ *
+ *
+ *
+ * val a = """
+ * Hello World!
+ * How are you?
+ * """.trimMargin()
+ *
+ * val a = """
+ * Hello World!
+ * How are you?
+ * """.trimMargin()
+ *
+ */
+class MultilineRawStringIndentation(val config: Config) : Rule(config) {
+ override val issue = Issue(
+ javaClass.simpleName,
+ Severity.Style,
+ "The indentation of the raw String should be consistent",
+ Debt.FIVE_MINS
+ )
+
+ @Configuration("indentation size")
+ private val indentSize by config(4)
+
+ @Suppress("ReturnCount")
+ override fun visitStringTemplateExpression(expression: KtStringTemplateExpression) {
+ super.visitStringTemplateExpression(expression)
+
+ val text = expression.text
+ val lineCount = text.lines().count()
+ if (lineCount <= 1) return
+ if (!expression.isTrimmed()) return
+ if (!text.matches(rawStringRegex)) return
+
+ val lineAndColumn = getLineAndColumnInPsiFile(expression.containingFile, expression.textRange) ?: return
+
+ expression.checkIndentation(
+ baseIndent = lineAndColumn.lineContent?.countIndent() ?: return,
+ firstLineNumber = lineAndColumn.line,
+ lastLineNumber = lineAndColumn.line + lineCount - 1
+ )
+ }
+
+ private fun KtStringTemplateExpression.checkIndentation(
+ baseIndent: Int,
+ firstLineNumber: Int,
+ lastLineNumber: Int,
+ ) {
+ checkContent(desiredIndent = baseIndent + indentSize, (firstLineNumber + 1)..(lastLineNumber - 1))
+ checkClosing(baseIndent, lastLineNumber)
+ }
+
+ private fun KtStringTemplateExpression.checkContent(
+ desiredIndent: Int,
+ lineNumberRange: IntRange,
+ ) {
+ val indentation = lineNumberRange
+ .map { lineNumber ->
+ val line = containingFile.getLine(lineNumber)
+ Triple(lineNumber, line, line.countIndent())
+ }
+
+ if (indentation.isNotEmpty()) {
+ indentation
+ .filter { (_, line, currentIndent) -> line.isNotEmpty() && currentIndent < desiredIndent }
+ .onEach { (lineNumber, line, currentIndent) ->
+ val location = containingFile.getLocation(
+ SourceLocation(lineNumber, if (line.isBlank()) 1 else currentIndent + 1),
+ SourceLocation(lineNumber, line.length + 1)
+ )
+
+ report(this, location, message(desiredIndent, currentIndent))
+ }
+ .ifEmpty {
+ if (indentation.none { (_, _, currentIndent) -> currentIndent == desiredIndent }) {
+ val location = containingFile.getLocation(
+ SourceLocation(lineNumberRange.first, desiredIndent + 1),
+ SourceLocation(lineNumberRange.last, indentation.last().second.length + 1),
+ )
+
+ report(
+ this,
+ location,
+ message(desiredIndent, indentation.minOf { (_, _, indent) -> indent }),
+ )
+ }
+ }
+ }
+ }
+
+ private fun KtStringTemplateExpression.checkClosing(
+ desiredIndent: Int,
+ lineNumber: Int,
+ ) {
+ val currentIndent = containingFile.getLine(lineNumber).countIndent()
+ if (currentIndent != desiredIndent) {
+ val location = if (currentIndent < desiredIndent) {
+ containingFile.getLocation(
+ SourceLocation(lineNumber, currentIndent + 1),
+ SourceLocation(lineNumber, currentIndent + "\"\"\"".length + 1),
+ )
+ } else {
+ containingFile.getLocation(
+ SourceLocation(lineNumber, desiredIndent + 1),
+ SourceLocation(lineNumber, currentIndent + 1),
+ )
+ }
+
+ report(this, location, message(desiredIndent, currentIndent))
+ }
+ }
+}
+
+private fun Rule.report(element: KtElement, location: Location, message: String) {
+ report(CodeSmell(issue, Entity.from(element, location), message))
+}
+
+private fun message(desiredIntent: Int, currentIndent: Int): String {
+ return "The indentation should be $desiredIntent but it is $currentIndent."
+}
+
+private val rawStringRegex = "\"\"\"\n(.|\n)*\n *\"\"\"".toRegex()
+
+private fun String.countIndent() = this.takeWhile { it == ' ' }.count()
+
+private fun PsiFile.getLine(line: Int): String {
+ return text.lineSequence().drop(line - 1).first()
+}
+
+private fun PsiFile.getLocation(start: SourceLocation, end: SourceLocation): Location {
+ val lines = this.text.lines()
+ var startOffset = 0
+ for (i in 1 until start.line) {
+ startOffset += lines[i - 1].length + 1
+ }
+ var endOffset = startOffset
+ for (i in start.line until end.line) {
+ endOffset += lines[i - 1].length + 1
+ }
+ this.text.lines()
+ return Location(
+ start,
+ end,
+ TextLocation(startOffset + start.column - 1, endOffset + end.column - 1),
+ toFilePath()
+ )
+}
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt
index 834e95b2f7d7..fac5238d873e 100644
--- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/StyleGuideProvider.kt
@@ -96,6 +96,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
RedundantHigherOrderMapUsage(config),
UseIfEmptyOrIfBlank(config),
MultilineLambdaItParameter(config),
+ MultilineRawStringIndentation(config),
UseIsNullOrEmpty(config),
UseOrEmpty(config),
UseAnyOrNoneInsteadOfFind(config),
diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentationSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentationSpec.kt
new file mode 100644
index 000000000000..6480c16b6fd1
--- /dev/null
+++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MultilineRawStringIndentationSpec.kt
@@ -0,0 +1,330 @@
+package io.gitlab.arturbosch.detekt.rules.style
+
+import io.gitlab.arturbosch.detekt.api.Config
+import io.gitlab.arturbosch.detekt.test.assertThat
+import io.gitlab.arturbosch.detekt.test.compileAndLint
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+class MultilineRawStringIndentationSpec {
+ val subject = MultilineRawStringIndentation(Config.empty)
+
+ @Nested
+ inner class IfTheOpeningDoesNotStartTheLine {
+ @Test
+ fun `raise multiline raw string without indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(2 to 1, 2 to 13)
+ .hasTextLocations("Hello world!")
+ }
+
+ @Test
+ fun `raise multiline raw strings without indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(2)
+ .hasTextLocations("Hello world!", "How are you?")
+ }
+
+ @Test
+ fun `raise multiline raw strings without right indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(2 to 2, 2 to 14)
+ .hasTextLocations("Hello world!")
+ }
+
+ @Test
+ fun `raise multiline raw strings with too much indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(2 to 5, 3 to 18)
+ .hasTextLocations(" Hello world!\n How are you?")
+ }
+
+ @Test
+ fun `don't raise multiline raw strings if one has correct indentation and the other more`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `don't raise multiline raw strings if all have the correct indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `don't raise multiline raw strings if all have the correct indentation or empty`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `raise multiline raw strings if a blank line doesn't have the minimum indentation`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(3 to 1, 3 to 3)
+ }
+
+ @Test
+ fun `raise multiline raw strings with indentation on closing`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(4 to 1, 4 to 5)
+ }
+ }
+
+ @Nested
+ inner class IfTheOpeningStartTheLine {
+ @Test
+ fun `raise multiline raw string without indentation`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(3 to 5, 3 to 17)
+ }
+
+ @Test
+ fun `raise multiline raw strings without indentation`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(2)
+ }
+
+ @Test
+ fun `raise multiline raw strings without right indentation`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(3 to 6, 3 to 18)
+ }
+
+ @Test
+ fun `raise multiline raw strings with too much indentation`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(3 to 9, 4 to 22)
+ .hasTextLocations(" Hello world!\n How are you?")
+ }
+
+ @Test
+ fun `don't raise multiline raw strings if one has correct indentation and the other more`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `don't raise multiline raw strings if all have the correct indentation`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `raise multiline raw strings with too much indentation on closing`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(5 to 5, 5 to 9)
+ }
+
+ @Test
+ fun `raise multiline raw strings with too little indentation on closing`() {
+ val code = """
+ val a =
+ $TQ
+ Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .hasSize(1)
+ .hasSourceLocation(5 to 3, 5 to 6)
+ }
+ }
+
+ @Nested
+ inner class CasesThatShouldBeIgnored {
+ @Test
+ fun `doesn't raise multiline raw strings without trim`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ $TQ
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings).isEmpty()
+ }
+
+ @Test
+ fun `don't raise one line raw strings`() {
+ val code = """
+ val a = ${TQ}Hello world!$TQ
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings).isEmpty()
+ }
+
+ @Test
+ fun `doesn't raise if it is not a raw string`() {
+ val code = """
+ val a = "Hello world!"
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings).isEmpty()
+ }
+
+ @Test
+ fun `don't raise if it contains content after the opening triple quote`() {
+ val code = """
+ val a = ${TQ}Hello world!
+ How are you?
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `don't raise if it contains content before the closing triple quote`() {
+ val code = """
+ val a = $TQ
+ Hello world!
+ How are you?$TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+
+ @Test
+ fun `don't raise if it isEmpty`() {
+ val code = """
+ val a = $TQ
+ $TQ.trimIndent()
+ """
+ subject.compileAndLint(code)
+ assertThat(subject.findings)
+ .isEmpty()
+ }
+ }
+}
+
+private const val TQ = "\"\"\""