InconsistentTextBlockIndentInspection: added inspection to detect text blocks with tabs and spaces in indent (IDEA-254690)

GitOrigin-RevId: 71721bc908e6226e7d1dfcfcd2b4ab3c435337e2
This commit is contained in:
Artemiy Sartakov
2020-12-02 11:00:31 +07:00
committed by intellij-monorepo-bot
parent aa20e91e37
commit f4feef1a9e
17 changed files with 455 additions and 2 deletions

View File

@@ -1448,6 +1448,12 @@
groupKey="group.names.performance.issues" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.bulkOperation.UseBulkOperationInspection"
key="inspection.use.bulk.operation.display.name" bundle="messages.JavaBundle"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA" shortName="InconsistentTextBlockIndent"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids15" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.InconsistentTextBlockIndentInspection"
bundle="messages.JavaBundle"
key="inspection.inconsistent.text.block.indent.name"/>
<localInspection groupPath="Java" language="JAVA" shortName="SimplifyCollector"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.declaration.redundancy" enabledByDefault="true" level="WARNING"

View File

@@ -0,0 +1,269 @@
// 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.codeInspection;
import com.google.common.base.Strings;
import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.SmartList;
import com.intellij.util.text.MergingCharSequence;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import static com.intellij.util.ObjectUtils.tryCast;
public class InconsistentTextBlockIndentInspection extends AbstractBaseJavaLocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!HighlightingFeature.TEXT_BLOCKS.isAvailable(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
return new JavaElementVisitor() {
@Override
public void visitLiteralExpression(PsiLiteralExpression expression) {
String[] lines = PsiLiteralUtil.getTextBlockLines(expression);
if (lines == null) return;
MixedIndentModel indentModel = MixedIndentModel.create(lines);
if (indentModel == null) return;
int tabSize = CodeStyle.getSettings(expression.getProject()).getTabSize(JavaFileType.INSTANCE);
int desiredIndent = indentModel.findDesiredIndent(tabSize);
int indexToReport = indentModel.findFirstInconsistentCharIdx(desiredIndent, tabSize);
if (indexToReport == -1) return;
List<LocalQuickFix> fixes = new SmartList<>(new MakeIndentConsistentFix(IndentType.SPACES),
new MakeIndentConsistentFix(IndentType.TABS));
indentModel.findAvailableIndentTypes(desiredIndent, tabSize)
.forEach(type -> fixes.add(new MakeIndentConsistentFix(tabSize, type)));
int start = expression.getText().indexOf('\n');
if (start == -1) return;
start++;
holder.registerProblem(expression,
new TextRange(start + indexToReport, start + indexToReport + 1),
JavaBundle.message("inspection.inconsistent.text.block.indent.message"),
fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
}
};
}
private enum IndentType {
SPACES,
TABS;
private static @Nullable IndentType of(char c) {
if (c == ' ') return SPACES;
if (c == '\t') return TABS;
return null;
}
}
private static class MakeIndentConsistentFix implements LocalQuickFix {
private final int myTabSize;
private final @NotNull IndentType myDesiredIndentType;
private MakeIndentConsistentFix(int tabSize, @NotNull IndentType indentType) {
myTabSize = tabSize;
myDesiredIndentType = indentType;
}
private MakeIndentConsistentFix(@NotNull IndentType indentType) {
this(1, indentType);
}
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
String message;
if (myDesiredIndentType == IndentType.TABS) {
message = myTabSize == 1 ?
JavaBundle.message("inspection.inconsistent.text.block.indent.spaces.to.tabs.one.to.one.fix") :
JavaBundle.message("inspection.inconsistent.text.block.indent.spaces.to.tabs.many.to.one.fix", myTabSize);
}
else {
message = myTabSize == 1 ?
JavaBundle.message("inspection.inconsistent.text.block.indent.tabs.to.spaces.one.to.one.fix") :
JavaBundle.message("inspection.inconsistent.text.block.indent.tabs.to.spaces.one.to.many.fix", myTabSize);
}
return message;
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiLiteralExpression literalExpression = tryCast(descriptor.getPsiElement(), PsiLiteralExpression.class);
if (literalExpression == null || !literalExpression.isTextBlock()) return;
if (!CommonRefactoringUtil.checkReadOnlyStatus(project, literalExpression)) return;
String[] lines = PsiLiteralUtil.getTextBlockLines(literalExpression);
if (lines == null) return;
MixedIndentModel indentModel = MixedIndentModel.create(lines);
if (indentModel == null) return;
int desiredIndent = indentModel.findDesiredIndent(myTabSize);
if (desiredIndent == -1) return;
StringBuilder newTextBlock = new StringBuilder();
newTextBlock.append("\"\"\"\n");
String indentText = myDesiredIndentType == IndentType.SPACES ?
Strings.repeat(" ", desiredIndent) : Strings.repeat("\t", desiredIndent / myTabSize);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (i != 0) newTextBlock.append('\n');
if (!isContentPart(lines, i, line)) {
newTextBlock.append(line);
continue;
}
int lineIndent = MixedIndentModel.findLineIndent(line, desiredIndent, myTabSize, myDesiredIndentType);
if (lineIndent == -1) return;
MergingCharSequence newLine = StringUtil.replaceSubSequence(line, 0, lineIndent, indentText);
newTextBlock.append(newLine);
}
newTextBlock.append("\"\"\"");
PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(project);
PsiExpression replacement = elementFactory.createExpressionFromText(newTextBlock.toString(), literalExpression);
CodeStyleManager manager = CodeStyleManager.getInstance(project);
manager.performActionWithFormatterDisabled((Runnable)() -> WriteAction.run(() -> literalExpression.replace(replacement)));
}
@Override
public boolean startInWriteAction() {
return false;
}
}
private static class MixedIndentModel {
private final String @NotNull [] myLines;
private MixedIndentModel(String @NotNull [] lines) {
myLines = lines;
}
private int findDesiredIndent(int tabSize) {
int desiredIndent = Integer.MAX_VALUE;
for (int i = 0; i < myLines.length; i++) {
String line = myLines[i];
if (!isContentPart(myLines, i, line)) continue;
int lineIndent = 0;
for (int j = 0; j < line.length(); j++) {
char c = line.charAt(j);
if (c == ' ') lineIndent++;
else if (c == '\t') lineIndent += tabSize;
else break;
}
if (lineIndent < desiredIndent) desiredIndent = lineIndent;
}
return desiredIndent;
}
private int findFirstInconsistentCharIdx(int desiredIndent, int tabSize) {
int pos = 0;
IndentType indentType = null;
for (int i = 0; i < myLines.length; i++) {
if (i != 0) pos++;
String line = myLines[i];
if (!isContentPart(myLines, i, line)) {
if (!line.isEmpty()) pos += line.length();
continue;
}
int indentToSee = desiredIndent;
for (int j = 0; j < line.length(); j++) {
if (indentToSee <= 0) break;
char c = line.charAt(j);
if (c == ' ') indentToSee--;
else if (c == '\t') indentToSee -= tabSize;
else return -1;
IndentType curIndentType = Objects.requireNonNull(IndentType.of(c));
if (indentType == null) {
indentType = curIndentType;
}
else if (indentType != curIndentType) {
return pos + j;
}
}
pos += line.length();
}
return -1;
}
private @NotNull List<IndentType> findAvailableIndentTypes(int desiredIndent, int tabSize) {
List<IndentType> 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;
}
}

View File

@@ -0,0 +1,24 @@
<html>
<body>
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.
<p>For example (here spaces are visualized as <code>·</code> and tabs as <code></code>, one tab takes 4 spaces in editor):</p>
<pre><code>
String colors = """
····················red
␉ ␉ ␉ ␉ ␉ green
····················blue""";
</code></pre>
<p>After printing such string the result would be:</p>
<pre><code>
red
···············green
blue
</code></pre>
<!-- tooltip end -->
<p>This inspection only reports if the configured language level is 15 or higher.</p>
<p><small>New in 2021.1</small></p>
</body>
</html>

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
class Foo {
void test() {
String a = """
""";
}
}

View File

@@ -0,0 +1,10 @@
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
class Foo {
void test() {
String colors = """
red
green
blue""";
}
}

View File

@@ -0,0 +1,20 @@
// "Replace spaces with tabs (1 space = 1 tab)" "true"
class Foo {
void test() {
String colors = """
red
green
blue
""";

View File

@@ -0,0 +1,9 @@
// "Replace spaces with tabs (1 space = 1 tab)" "false"
class Foo {
void test() {
String colors = """
<caret> """;
}
}

View File

@@ -0,0 +1,11 @@
// "Replace tabs with spaces (1 tab = 4 spaces)" "false"
class Foo {
void test() {
String colors = """
red
<caret> green
blue""";
}
}

View File

@@ -0,0 +1,8 @@
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
class Foo {
void test() {
String a = """
<caret> """;
}
}

View File

@@ -0,0 +1,10 @@
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
class Foo {
void test() {
String colors = """
red
<caret> green
blue""";
}
}

View File

@@ -0,0 +1,11 @@
// "Replace spaces with tabs (1 space = 1 tab)" "false"
class Foo {
void test() {
String a = """
red
blue
<caret> green
""";
}
}

View File

@@ -0,0 +1,11 @@
// "Replace spaces with tabs (1 space = 1 tab)" "false"
class Foo {
void test() {
String a = """
red
blue
<caret> green
""";
}
}

View File

@@ -0,0 +1,10 @@
// "Replace spaces with tabs (4 spaces = 1 tab)" "false"
class Foo {
void test() {
String colors = """
red
<caret> green
blue""";
}
}

View File

@@ -0,0 +1,20 @@
// "Replace spaces with tabs (1 space = 1 tab)" "true"
class Foo {
void test() {
String colors = """
red
green
blue
<caret> """;

View File

@@ -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";
}
}

View File

@@ -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)