Skip to content

Commit

Permalink
Implement MultilineRawStringIndentation
Browse files Browse the repository at this point in the history
  • Loading branch information
BraisGabin committed Jul 20, 2022
1 parent 1db1c5a commit 37ac386
Show file tree
Hide file tree
Showing 4 changed files with 519 additions and 0 deletions.
3 changes: 3 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Expand Up @@ -597,6 +597,9 @@ style:
active: true
MultilineLambdaItParameter:
active: false
MultilineRawStringIndentation:
active: false
indentSize: 4
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
Expand Down
@@ -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.
*
* <noncompliant>
* val a = """
* Hello World!
* How are you?
* """.trimMargin()
*
* val a = """
* Hello World!
* How are you?
* """.trimMargin()
* </noncompliant>
*
* <compliant>
* val a = """
* Hello World!
* How are you?
* """.trimMargin()
*
* val a = """
* Hello World!
* How are you?
* """.trimMargin()
* </compliant>
*/
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()
)
}
Expand Up @@ -96,6 +96,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
RedundantHigherOrderMapUsage(config),
UseIfEmptyOrIfBlank(config),
MultilineLambdaItParameter(config),
MultilineRawStringIndentation(config),
UseIsNullOrEmpty(config),
UseOrEmpty(config),
UseAnyOrNoneInsteadOfFind(config),
Expand Down

0 comments on commit 37ac386

Please sign in to comment.