indentTypes = new SmartList<>(IndentType.SPACES, IndentType.TABS);
+ for (int i = 0; i < myLines.length; i++) {
+ String line = myLines[i];
+ if (!isContentPart(myLines, i, line)) continue;
+ int indentToSee = desiredIndent;
+ int nSpaces = 0;
+ for (int j = 0; j < line.length(); j++) {
+ char c = line.charAt(j);
+ if (c == ' ') {
+ indentToSee--;
+ nSpaces++;
+ }
+ else if (c == '\t') {
+ if (nSpaces % tabSize != 0) {
+ indentTypes.remove(IndentType.TABS);
+ }
+ indentToSee -= tabSize;
+ nSpaces = 0;
+ }
+ if (indentToSee <= 0) break;
+ }
+ if (nSpaces % tabSize != 0) indentTypes.remove(IndentType.TABS);
+ if (indentToSee < 0) indentTypes.remove(IndentType.SPACES);
+ }
+ return indentTypes;
+ }
+
+ private static int findLineIndent(@NotNull String line, int desiredIndent, int tabSize, @NotNull IndentType desiredIndentType) {
+ int i;
+ int nSpaces = 0;
+ for (i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+ if (c == ' ') {
+ nSpaces++;
+ desiredIndent--;
+ }
+ else if (c == '\t') {
+ if (desiredIndentType == IndentType.TABS && nSpaces % tabSize != 0) {
+ return -1;
+ }
+ nSpaces = 0;
+ desiredIndent -= tabSize;
+ }
+ if (desiredIndent <= 0) break;
+ }
+ if (desiredIndentType == IndentType.TABS && nSpaces % tabSize != 0) return -1;
+ return desiredIndent == 0 ? i + 1 : -1;
+ }
+
+ private static @Nullable MixedIndentModel create(String @NotNull [] lines) {
+ int indent = PsiLiteralUtil.getTextBlockIndent(lines, true, false);
+ if (indent <= 0) return null;
+ IndentType indentType = null;
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ if (!isContentPart(lines, i, line)) continue;
+ for (int j = 0; j < indent; j++) {
+ IndentType curIndentType = IndentType.of(line.charAt(j));
+ if (indentType == null) {
+ indentType = curIndentType;
+ continue;
+ }
+ if (curIndentType != indentType) {
+ return new MixedIndentModel(lines);
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ private static boolean isContentPart(String @NotNull [] lines, int i, String line) {
+ return !line.isBlank() || i == lines.length - 1;
+ }
+}
diff --git a/java/java-impl/src/inspectionDescriptions/InconsistentTextBlockIndent.html b/java/java-impl/src/inspectionDescriptions/InconsistentTextBlockIndent.html
new file mode 100644
index 000000000000..28ca1b31e0e7
--- /dev/null
+++ b/java/java-impl/src/inspectionDescriptions/InconsistentTextBlockIndent.html
@@ -0,0 +1,24 @@
+
+
+Reports text blocks with indent that consists of both spaces and tabs.
+Such cases might be misleading since one space is treated as one tab during text block processing.
+
+For example (here spaces are visualized as · and tabs as ␉, one tab takes 4 spaces in editor):
+
+String colors = """
+····················red
+␉ ␉ ␉ ␉ ␉ green
+····················blue""";
+
+
+After printing such string the result would be:
+
+red
+···············green
+blue
+
+
+This inspection only reports if the configured language level is 15 or higher.
+New in 2021.1
+
+
\ No newline at end of file
diff --git a/java/java-psi-api/src/com/intellij/psi/util/PsiLiteralUtil.java b/java/java-psi-api/src/com/intellij/psi/util/PsiLiteralUtil.java
index 8d424aa34aa5..73f2be911e6c 100644
--- a/java/java-psi-api/src/com/intellij/psi/util/PsiLiteralUtil.java
+++ b/java/java-psi-api/src/com/intellij/psi/util/PsiLiteralUtil.java
@@ -391,7 +391,7 @@ public final class PsiLiteralUtil {
* @param expression a text block expression
* @return the lines of the expression, or null if the expression is not a text block.
*/
- public static String @Nullable [] getTextBlockLines(PsiLiteralExpression expression) {
+ public static String @Nullable [] getTextBlockLines(@NotNull PsiLiteralExpression expression) {
if (!expression.isTextBlock()) return null;
String rawText = expression.getText();
if (rawText.length() < 7 || !rawText.endsWith("\"\"\"")) return null;
@@ -412,7 +412,7 @@ public final class PsiLiteralUtil {
* @param expression a text block literal expression
* @return the indent of the text block counted in characters, where a tab is also counted as 1.
*/
- public static int getTextBlockIndent(PsiLiteralExpression expression) {
+ public static int getTextBlockIndent(@NotNull PsiLiteralExpression expression) {
String[] lines = getTextBlockLines(expression);
if (lines == null) return -1;
return getTextBlockIndent(lines);
@@ -427,6 +427,7 @@ public final class PsiLiteralUtil {
/**
* @see #getTextBlockIndent(PsiLiteralExpression)
+ * Note that this method might change some of the given lines.
*/
public static int getTextBlockIndent(String @NotNull [] lines, boolean preserveContent, boolean ignoreLastLine) {
int prefix = Integer.MAX_VALUE;
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterOneLineBlock.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterOneLineBlock.java
new file mode 100644
index 000000000000..e0a41a868f51
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterOneLineBlock.java
@@ -0,0 +1,8 @@
+// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
+
+class Foo {
+ void test() {
+ String a = """
+ """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterSimple.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterSimple.java
new file mode 100644
index 000000000000..01872c45fd69
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterSimple.java
@@ -0,0 +1,10 @@
+// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
+
+class Foo {
+ void test() {
+ String colors = """
+ red
+ green
+ blue""";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterWithBlankLines.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterWithBlankLines.java
new file mode 100644
index 000000000000..c635507aee69
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/afterWithBlankLines.java
@@ -0,0 +1,20 @@
+// "Replace spaces with tabs (1 space = 1 tab)" "true"
+
+class Foo {
+ void test() {
+ String colors = """
+
+
+ red
+
+
+
+ green
+
+
+
+ blue
+
+
+ """;
+
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeBlank.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeBlank.java
new file mode 100644
index 000000000000..d579c9c858ab
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeBlank.java
@@ -0,0 +1,9 @@
+// "Replace spaces with tabs (1 space = 1 tab)" "false"
+
+class Foo {
+ void test() {
+ String colors = """
+
+ """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeDesiredIndentIntersectsTabFromContent.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeDesiredIndentIntersectsTabFromContent.java
new file mode 100644
index 000000000000..47b9ee2b9545
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeDesiredIndentIntersectsTabFromContent.java
@@ -0,0 +1,11 @@
+// "Replace tabs with spaces (1 tab = 4 spaces)" "false"
+
+class Foo {
+ void test() {
+ String colors = """
+ red
+ green
+ blue""";
+
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeOneLineBlock.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeOneLineBlock.java
new file mode 100644
index 000000000000..3afde9a5f4ef
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeOneLineBlock.java
@@ -0,0 +1,8 @@
+// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
+
+class Foo {
+ void test() {
+ String a = """
+ """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSimple.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSimple.java
new file mode 100644
index 000000000000..af2ec46a62f7
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSimple.java
@@ -0,0 +1,10 @@
+// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
+
+class Foo {
+ void test() {
+ String colors = """
+ red
+ green
+ blue""";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSpacesOnly.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSpacesOnly.java
new file mode 100644
index 000000000000..7d97555f44aa
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeSpacesOnly.java
@@ -0,0 +1,11 @@
+// "Replace spaces with tabs (1 space = 1 tab)" "false"
+
+class Foo {
+ void test() {
+ String a = """
+ red
+ blue
+ green
+ """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeTabsOnly.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeTabsOnly.java
new file mode 100644
index 000000000000..ae9f7164185c
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeTabsOnly.java
@@ -0,0 +1,11 @@
+// "Replace spaces with tabs (1 space = 1 tab)" "false"
+
+class Foo {
+ void test() {
+ String a = """
+ red
+ blue
+ green
+ """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeUnevenNumberOfSpaces.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeUnevenNumberOfSpaces.java
new file mode 100644
index 000000000000..672374e51bd1
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeUnevenNumberOfSpaces.java
@@ -0,0 +1,10 @@
+// "Replace spaces with tabs (4 spaces = 1 tab)" "false"
+
+class Foo {
+ void test() {
+ String colors = """
+ red
+ green
+ blue""";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeWithBlankLines.java b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeWithBlankLines.java
new file mode 100644
index 000000000000..4cd76f3ffa81
--- /dev/null
+++ b/java/java-tests/testData/inspection/inconsistentTextBlockIndent/beforeWithBlankLines.java
@@ -0,0 +1,20 @@
+// "Replace spaces with tabs (1 space = 1 tab)" "true"
+
+class Foo {
+ void test() {
+ String colors = """
+
+
+ red
+
+
+
+ green
+
+
+
+ blue
+
+
+ """;
+
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/InconsistentTextBlockIndentInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/InconsistentTextBlockIndentInspectionTest.java
new file mode 100644
index 000000000000..de23bd8b7f59
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/InconsistentTextBlockIndentInspectionTest.java
@@ -0,0 +1,19 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.java.codeInspection;
+
+import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
+import com.intellij.codeInspection.InconsistentTextBlockIndentInspection;
+import com.intellij.codeInspection.LocalInspectionTool;
+import org.jetbrains.annotations.NotNull;
+
+public class InconsistentTextBlockIndentInspectionTest extends LightQuickFixParameterizedTestCase {
+ @Override
+ protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
+ return new LocalInspectionTool[]{new InconsistentTextBlockIndentInspection()};
+ }
+
+ @Override
+ protected String getBasePath() {
+ return "/inspection/inconsistentTextBlockIndent";
+ }
+}
diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties
index 104c77348a06..439200d8445b 100644
--- a/java/openapi/resources/messages/JavaBundle.properties
+++ b/java/openapi/resources/messages/JavaBundle.properties
@@ -697,6 +697,12 @@ inspection.text.block.backward.migration.name=Text block can be replaced with re
inspection.text.block.migration.message={0} can be replaced with text block
inspection.text.block.migration.name=Text block can be used
inspection.text.block.migration.suggest.literal.replacement=Apply to single string literals
+inspection.inconsistent.text.block.indent.name=Inconsistent white space indentation
+inspection.inconsistent.text.block.indent.message=Text block indent consists of tabs and spaces
+inspection.inconsistent.text.block.indent.spaces.to.tabs.one.to.one.fix=Replace spaces with tabs (1 space = 1 tab)
+inspection.inconsistent.text.block.indent.spaces.to.tabs.many.to.one.fix=Replace spaces with tabs ({0} spaces = 1 tab)
+inspection.inconsistent.text.block.indent.tabs.to.spaces.one.to.one.fix=Replace tabs with spaces (1 tab = 1 space)
+inspection.inconsistent.text.block.indent.tabs.to.spaces.one.to.many.fix=Replace tabs with spaces (1 tab = {0} spaces)
inspection.undeclared.service.usage.message=Usage of service ''{0}'' is not declared in module-info
inspection.undeclared.service.usage.name=Usage of service not declared in 'module-info'
inspection.unused.assignment.option=Report ++i when may be replaced with (i + 1)