2
2
* Main logic of indentation including Rule and utility classes and methods.
3
3
*/
4
4
5
+ @file:Suppress(" FILE_UNORDERED_IMPORTS" )// False positives, see #1494.
6
+
5
7
package org.cqfn.diktat.ruleset.rules.chapter3.files
6
8
7
9
import org.cqfn.diktat.common.config.rules.RulesConfig
@@ -79,11 +81,13 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
79
81
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
80
82
import org.jetbrains.kotlin.psi.psiUtil.startOffset
81
83
82
- import java.util.ArrayDeque as Stack
83
-
84
+ import kotlin.contracts.ExperimentalContracts
85
+ import kotlin.contracts.contract
84
86
import kotlin.math.abs
85
87
import kotlin.reflect.KCallable
86
88
89
+ import java.util.ArrayDeque as Stack
90
+
87
91
/* *
88
92
* Rule that checks indentation. The following general rules are checked:
89
93
* 1. Only spaces should be used each indentation is equal to 4 spaces
@@ -198,21 +202,6 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
198
202
}
199
203
}
200
204
201
- private fun isCloseAndOpenQuoterOffset (nodeWhiteSpace : ASTNode , expectedIndent : Int ): Boolean {
202
- val nextNode = nodeWhiteSpace.treeNext
203
- if (nextNode.elementType == VALUE_ARGUMENT ) {
204
- val nextNodeDot = getNextDotExpression(nextNode)
205
- nextNodeDot?.getFirstChildWithType(STRING_TEMPLATE )?.let {
206
- if (it.getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY ).size > 1 ) {
207
- val closingQuote = it.getFirstChildWithType(CLOSING_QUOTE )?.treePrev?.text
208
- ?.length ? : - 1
209
- return expectedIndent == closingQuote
210
- }
211
- }
212
- }
213
- return true
214
- }
215
-
216
205
@Suppress(" ForbiddenComment" )
217
206
private fun IndentContext.visitWhiteSpace (astNode : ASTNode ) {
218
207
require(astNode.isMultilineWhitespace()) {
@@ -242,10 +231,10 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
242
231
addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false )
243
232
}
244
233
245
- val difOffsetCloseAndOpenQuote = isCloseAndOpenQuoterOffset (astNode, indentError.actual)
234
+ val alignedOpeningAndClosingQuotes = hasAlignedOpeningAndClosingQuotes (astNode, indentError.actual)
246
235
247
- if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || ! difOffsetCloseAndOpenQuote ) {
248
- val warnText = if (! difOffsetCloseAndOpenQuote ) {
236
+ if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || ! alignedOpeningAndClosingQuotes ) {
237
+ val warnText = if (! alignedOpeningAndClosingQuotes ) {
249
238
" the same number of indents to the opening and closing quotes was expected"
250
239
} else {
251
240
" expected $expectedIndent but was ${indentError.actual} "
@@ -268,7 +257,7 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
268
257
expectedIndent : Int ,
269
258
actualIndent : Int
270
259
) {
271
- val nextNodeDot = getNextDotExpression( whiteSpace.node.treeNext)
260
+ val nextNodeDot = whiteSpace.node.treeNext.getNextDotExpression( )
272
261
if (nextNodeDot != null &&
273
262
nextNodeDot.elementType == DOT_QUALIFIED_EXPRESSION &&
274
263
nextNodeDot.firstChildNode.elementType == STRING_TEMPLATE &&
@@ -361,12 +350,6 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
361
350
}
362
351
}
363
352
364
- private fun getNextDotExpression (node : ASTNode ) = if (node.elementType == DOT_QUALIFIED_EXPRESSION ) {
365
- node
366
- } else {
367
- node.getFirstChildWithType(DOT_QUALIFIED_EXPRESSION )
368
- }
369
-
370
353
/* *
371
354
* Modifies [templateEntry] by correcting its indentation level.
372
355
*
@@ -734,11 +717,30 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
734
717
private fun ASTNode.isMultilineWhitespace (): Boolean =
735
718
elementType == WHITE_SPACE && textContains(NEWLINE )
736
719
720
+ @OptIn(ExperimentalContracts ::class )
721
+ private fun ASTNode?.isMultilineStringTemplate (): Boolean {
722
+ contract {
723
+ returns(true ) implies (this @isMultilineStringTemplate != null )
724
+ }
725
+
726
+ this ? : return false
727
+
728
+ return elementType == STRING_TEMPLATE &&
729
+ getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY ).any { entry ->
730
+ entry.textContains(NEWLINE )
731
+ }
732
+ }
733
+
737
734
/* *
738
735
* @return `true` if this is a [String.trimIndent] or [String.trimMargin]
739
736
* call, `false` otherwise.
740
737
*/
738
+ @OptIn(ExperimentalContracts ::class )
741
739
private fun ASTNode?.isTrimIndentOrMarginCall (): Boolean {
740
+ contract {
741
+ returns(true ) implies (this @isTrimIndentOrMarginCall != null )
742
+ }
743
+
742
744
this ? : return false
743
745
744
746
require(elementType == CALL_EXPRESSION ) {
@@ -758,6 +760,12 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
758
760
return identifier.text in knownTrimFunctionPatterns
759
761
}
760
762
763
+ private fun ASTNode.getNextDotExpression (): ASTNode ? =
764
+ when (elementType) {
765
+ DOT_QUALIFIED_EXPRESSION -> this
766
+ else -> getFirstChildWithType(DOT_QUALIFIED_EXPRESSION )
767
+ }
768
+
761
769
/* *
762
770
* @return the matching closing brace type for this opening brace type,
763
771
* or vice versa.
@@ -791,5 +799,64 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
791
799
this > 0 -> this
792
800
else -> 0
793
801
}
802
+
803
+ /* *
804
+ * Processes fragments like:
805
+ *
806
+ * ```kotlin
807
+ * f(
808
+ * """
809
+ * |foobar
810
+ * """.trimMargin()
811
+ * )
812
+ * ```
813
+ *
814
+ * @param whitespace the whitespace node between an [LPAR] and the
815
+ * `trimIndent()`- or `trimMargin()`- terminated string template, which is
816
+ * an effective argument of a function call. The string template is
817
+ * expected to begin on a separate line (otherwise, there'll be no
818
+ * whitespace in-between).
819
+ * @return `true` if the opening and the closing quotes of the string
820
+ * template are aligned, `false` otherwise.
821
+ */
822
+ private fun hasAlignedOpeningAndClosingQuotes (whitespace : ASTNode , expectedIndent : Int ): Boolean {
823
+ require(whitespace.isMultilineWhitespace()) {
824
+ " The node is $whitespace while a multi-line $WHITE_SPACE expected"
825
+ }
826
+
827
+ /*
828
+ * Here, we expect that `nextNode` is a VALUE_ARGUMENT which contains
829
+ * the dot-qualified expression (`STRING_TEMPLATE.trimIndent()` or
830
+ * `STRING_TEMPLATE.trimMargin()`).
831
+ */
832
+ val nextFunctionArgument = whitespace.treeNext
833
+ if (nextFunctionArgument.elementType == VALUE_ARGUMENT ) {
834
+ val memberOrExtensionCall = nextFunctionArgument.getNextDotExpression()
835
+
836
+ /*
837
+ * Limit allowed member or extension calls to `trimIndent()` and
838
+ * `trimMargin()`.
839
+ */
840
+ if (memberOrExtensionCall != null &&
841
+ memberOrExtensionCall.getFirstChildWithType(CALL_EXPRESSION ).isTrimIndentOrMarginCall()) {
842
+ val stringTemplate = memberOrExtensionCall.getFirstChildWithType(STRING_TEMPLATE )
843
+
844
+ /*
845
+ * Limit the logic to multi-line string templates only (the
846
+ * opening and closing quotes of a single-line template are,
847
+ * obviously, always mis-aligned).
848
+ */
849
+ if (stringTemplate != null && stringTemplate.isMultilineStringTemplate()) {
850
+ val closingQuoteIndent = stringTemplate.getFirstChildWithType(CLOSING_QUOTE )
851
+ ?.treePrev
852
+ ?.text
853
+ ?.length ? : - 1
854
+ return expectedIndent == closingQuoteIndent
855
+ }
856
+ }
857
+ }
858
+
859
+ return true
860
+ }
794
861
}
795
862
}
0 commit comments