mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
Java: rework "Inconsistent whitespace indentation in text block" inspection (IDEA-353100)
GitOrigin-RevId: f2b3042e93e5b9e94fc1b2ee1d7341dd9224d480
This commit is contained in:
committed by
intellij-monorepo-bot
parent
e90a4de2d9
commit
9838188e46
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInspection;
|
||||
|
||||
import com.intellij.application.options.CodeStyle;
|
||||
@@ -14,18 +14,12 @@ import com.intellij.psi.JavaElementVisitor;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiElementVisitor;
|
||||
import com.intellij.psi.PsiLiteralExpression;
|
||||
import com.intellij.psi.codeStyle.CodeStyleManager;
|
||||
import com.intellij.psi.util.PsiLiteralUtil;
|
||||
import com.intellij.refactoring.util.CommonRefactoringUtil;
|
||||
import com.intellij.util.SmartList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.intellij.util.ObjectUtils.tryCast;
|
||||
|
||||
public final class InconsistentTextBlockIndentInspection extends AbstractBaseJavaLocalInspectionTool {
|
||||
@Override
|
||||
public @NotNull Set<@NotNull JavaFeature> requiredFeatures() {
|
||||
@@ -38,161 +32,76 @@ public final class InconsistentTextBlockIndentInspection extends AbstractBaseJav
|
||||
@Override
|
||||
public void visitLiteralExpression(@NotNull PsiLiteralExpression expression) {
|
||||
String[] lines = PsiLiteralUtil.getTextBlockLines(expression);
|
||||
if (lines == null) return;
|
||||
int tabSize = CodeStyle.getSettings(expression.getProject()).getTabSize(JavaFileType.INSTANCE);
|
||||
if (lines == null || lines.length == 1) return;
|
||||
int indentLength = PsiLiteralUtil.getTextBlockIndent(expression);
|
||||
int indexToReport = -1;
|
||||
int offset = 0;
|
||||
String indent = null;
|
||||
for (String line : lines) {
|
||||
if (line.length() >= indentLength) {
|
||||
if (indent == null) {
|
||||
indent = line.substring(0, indentLength);
|
||||
}
|
||||
else {
|
||||
int mismatched = mismatch(indent, line.substring(0, indentLength));
|
||||
if (mismatched >= 0) {
|
||||
indexToReport = offset + mismatched;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += line.length() + 1; // length plus newline
|
||||
}
|
||||
if (indexToReport < 0) return;
|
||||
var styleSettings = CodeStyle.getSettings(expression.getProject());
|
||||
int tabSize = styleSettings.getTabSize(JavaFileType.INSTANCE);
|
||||
if (tabSize == 1) return;
|
||||
int start = expression.getText().indexOf('\n');
|
||||
if (start == -1) return;
|
||||
start++;
|
||||
MixedIndentModel indentModel = MixedIndentModel.create(lines);
|
||||
if (indentModel == null) return;
|
||||
int indexToReport = indentModel.myInconsistencyIdx;
|
||||
List<LocalQuickFix> fixes = new SmartList<>(new MakeIndentConsistentFix(IndentType.SPACES),
|
||||
new MakeIndentConsistentFix(IndentType.TABS),
|
||||
new MakeIndentConsistentFix(tabSize, IndentType.SPACES));
|
||||
if (indentModel.canReplaceWithTabs(tabSize)) {
|
||||
fixes.add(new MakeIndentConsistentFix(tabSize, IndentType.TABS));
|
||||
}
|
||||
boolean useTabCharacter = styleSettings.getIndentOptions(JavaFileType.INSTANCE).USE_TAB_CHARACTER;
|
||||
holder.registerProblem(expression,
|
||||
new TextRange(start + indexToReport, start + indexToReport + 1),
|
||||
JavaBundle.message("inspection.inconsistent.text.block.indent.message"),
|
||||
fixes.toArray(LocalQuickFix.EMPTY_ARRAY));
|
||||
new MakeIndentConsistentFix(useTabCharacter));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private enum IndentType {
|
||||
SPACES,
|
||||
TABS
|
||||
/**
|
||||
* Finds and returns the index of the first char mismatch between two Strings, otherwise return -1 if no mismatch is found.
|
||||
* The index will be in the range of 0 (inclusive) up to the length (inclusive) of the smaller String
|
||||
*
|
||||
* @param a the first String to be tested for a mismatch
|
||||
* @param b the second String to be tested for a mismatch
|
||||
* @return the index of the first char mismatch between the two String, otherwise {@code -1}.
|
||||
*/
|
||||
private static int mismatch(@NotNull String a, @NotNull String b) {
|
||||
int length = Math.min(a.length(), b.length());
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (a.charAt(i) != b.charAt(i)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static class MakeIndentConsistentFix extends PsiUpdateModCommandQuickFix {
|
||||
|
||||
private final int myTabSize;
|
||||
private final @NotNull IndentType myDesiredIndentType;
|
||||
private final boolean myUseTabs;
|
||||
|
||||
private MakeIndentConsistentFix(int tabSize, @NotNull IndentType indentType) {
|
||||
myTabSize = tabSize;
|
||||
myDesiredIndentType = indentType;
|
||||
}
|
||||
|
||||
private MakeIndentConsistentFix(@NotNull IndentType indentType) {
|
||||
this(1, indentType);
|
||||
private MakeIndentConsistentFix(boolean useTabs) {
|
||||
myUseTabs = useTabs;
|
||||
}
|
||||
|
||||
@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;
|
||||
return myUseTabs
|
||||
? JavaBundle.message("inspection.inconsistent.text.block.indent.tabs")
|
||||
: JavaBundle.message("inspection.inconsistent.text.block.indent.spaces");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
|
||||
PsiLiteralExpression literalExpression = tryCast(element, 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;
|
||||
String newTextBlock = indentModel.indentWith(myDesiredIndentType, myTabSize);
|
||||
if (newTextBlock == null) return;
|
||||
TrailingWhitespacesInTextBlockInspection.replaceTextBlock(literalExpression, "\"\"\"\n" + newTextBlock + "\"\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static class MixedIndentModel {
|
||||
|
||||
private final String[] myLines;
|
||||
private final int[] mySpaces;
|
||||
private final int[] myTabs;
|
||||
private final int myInconsistencyIdx;
|
||||
|
||||
private MixedIndentModel(String[] lines, int[] spaces, int[] tabs, int inconsistencyIdx) {
|
||||
myLines = lines;
|
||||
mySpaces = spaces;
|
||||
myTabs = tabs;
|
||||
myInconsistencyIdx = inconsistencyIdx;
|
||||
}
|
||||
|
||||
private boolean canReplaceWithTabs(int tabSize) {
|
||||
return Arrays.stream(mySpaces).allMatch(nSpaces -> nSpaces == -1 || nSpaces % tabSize == 0);
|
||||
}
|
||||
|
||||
private @Nullable String indentWith(@NotNull IndentType indentType, int tabSize) {
|
||||
StringBuilder indented = new StringBuilder();
|
||||
for (int i = 0; i < myLines.length; i++) {
|
||||
if (i != 0) indented.append('\n');
|
||||
String line = myLines[i];
|
||||
int nSpaces = mySpaces[i];
|
||||
if (nSpaces == -1) {
|
||||
indented.append(line);
|
||||
continue;
|
||||
}
|
||||
int nTabs = myTabs[i];
|
||||
String indent = createIndent(nSpaces, nTabs, indentType, tabSize);
|
||||
if (indent == null) return null;
|
||||
indented.append(indent);
|
||||
indented.append(line, nSpaces + nTabs, line.length());
|
||||
}
|
||||
return indented.toString();
|
||||
}
|
||||
|
||||
private static @Nullable String createIndent(int nSpaces, int nTabs, @NotNull IndentType indentType, int tabSize) {
|
||||
if (indentType == IndentType.SPACES) return " ".repeat(nSpaces + nTabs * tabSize);
|
||||
if (nSpaces % tabSize != 0) return null;
|
||||
return "\t".repeat(nTabs + nSpaces / tabSize);
|
||||
}
|
||||
|
||||
private static @Nullable MixedIndentModel create(String[] lines) {
|
||||
int indent = PsiLiteralUtil.getTextBlockIndent(lines, true, false);
|
||||
if (indent <= 0) return null;
|
||||
int[] spaces = new int[lines.length];
|
||||
int[] tabs = new int[lines.length];
|
||||
Character expectedIndentChar = null;
|
||||
int inconsistencyIdx = -1;
|
||||
int pos = 0;
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
if (i != 0) pos++;
|
||||
String line = lines[i];
|
||||
boolean isContentPart = !line.isBlank() || i == lines.length - 1;
|
||||
if (!isContentPart) {
|
||||
spaces[i] = -1;
|
||||
tabs[i] = -1;
|
||||
pos += line.length();
|
||||
continue;
|
||||
}
|
||||
if (expectedIndentChar == null) expectedIndentChar = line.charAt(0);
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
char c = line.charAt(j);
|
||||
if (c != ' ' && c != '\t') break;
|
||||
if (j < indent) inconsistencyIdx = getInconsistencyIndex(inconsistencyIdx, c, pos, j, expectedIndentChar);
|
||||
if (c == ' ') {
|
||||
spaces[i]++;
|
||||
}
|
||||
else {
|
||||
tabs[i]++;
|
||||
}
|
||||
}
|
||||
pos += line.length();
|
||||
}
|
||||
return inconsistencyIdx == -1 ? null : new MixedIndentModel(lines, spaces, tabs, inconsistencyIdx);
|
||||
}
|
||||
|
||||
private static int getInconsistencyIndex(int inconsistencyIdx, char c, int pos, int idx, @NotNull Character expectedIndentChar) {
|
||||
if (inconsistencyIdx != -1) return inconsistencyIdx;
|
||||
if (expectedIndentChar == c) return -1;
|
||||
return pos + idx;
|
||||
CodeStyleManager.getInstance(project).reformat(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<html>
|
||||
<body>
|
||||
Reports text blocks that are indented using both spaces and tabs.
|
||||
Such cases produce unexpected results since spaces and tabs are treated equally by the text block processing.
|
||||
Reports text blocks that are indented using both space and tab characters.
|
||||
This can produce unexpected results because spaces and tabs are treated equally by javac's text block processing.
|
||||
<p>In the following example, spaces and tabs are visualized as <code>·</code> and <code>␉</code> respectively,
|
||||
and a tab is equal to 4 spaces in the editor.</p>
|
||||
and a tab is equal to 2 spaces in the editor.</p>
|
||||
<p><b>Example:</b></p>
|
||||
<pre><code>
|
||||
String colors = """
|
||||
········red
|
||||
␉ ␉ green
|
||||
········blue""";
|
||||
····red
|
||||
␉ ␉ green
|
||||
····blue""";
|
||||
</code></pre>
|
||||
|
||||
<p>After printing such a string, the result will be:</p>
|
||||
<p>When printing such a string, the result will be:</p>
|
||||
<pre><code>
|
||||
······red
|
||||
··red
|
||||
green
|
||||
······blue
|
||||
··blue
|
||||
</code></pre>
|
||||
<p>After the compiler removes an equal amount of spaces or tabs from the beginning of each line,
|
||||
<p>After the compiler removes an equal number of whitespace characters from the beginning of each line,
|
||||
some lines remain with leading spaces.</p>
|
||||
<!-- tooltip end -->
|
||||
<p><small>New in 2021.1</small></p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String colors = """
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String a = """
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String colors = """
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String colors = """
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
red
|
||||
green
|
||||
blue""";
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
// "Replace spaces with tabs (1 space = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String colors = """
|
||||
|
||||
|
||||
red
|
||||
|
||||
|
||||
|
||||
green
|
||||
|
||||
|
||||
|
||||
blue
|
||||
|
||||
|
||||
""";
|
||||
|
||||
|
||||
red
|
||||
|
||||
|
||||
|
||||
green
|
||||
|
||||
|
||||
|
||||
blue
|
||||
|
||||
|
||||
""";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (1 space = 1 tab)" "false"
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String s = """
|
||||
<caret>foo
|
||||
bar""";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace tabs with spaces (1 tab = 4 spaces)" "true"
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (1 space = 1 tab)" "false"
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (4 spaces = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (1 space = 1 tab)" "false"
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// "Replace spaces with tabs (4 spaces = 1 tab)" "false"
|
||||
// "Indent text block with spaces only" "false"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
String colors = """
|
||||
red
|
||||
<caret> green
|
||||
<caret> green
|
||||
blue""";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// "Replace spaces with tabs (1 space = 1 tab)" "true"
|
||||
// "Indent text block with spaces only" "true"
|
||||
|
||||
class Foo {
|
||||
void test() {
|
||||
|
||||
@@ -780,11 +780,9 @@ inspection.implicit.to.explicit.class.backward.migration.fix.name=Convert implic
|
||||
inspection.explicit.to.implicit.class.migration.name=Explicit class declaration can be converted into implicitly declared class
|
||||
inspection.explicit.to.implicit.class.migration.fix.name=Convert into implicitly declared class
|
||||
inspection.inconsistent.text.block.indent.name=Inconsistent whitespace indentation in text block
|
||||
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.inconsistent.text.block.indent.message=Text block indent consists of mixed tabs and spaces
|
||||
inspection.inconsistent.text.block.indent.spaces=Indent text block with spaces only
|
||||
inspection.inconsistent.text.block.indent.tabs=Indent text block with tabs only
|
||||
inspection.suspicious.return.byte.input.stream.name = Suspicious byte value returned from 'InputStream.read()'
|
||||
inspection.suspicious.return.byte.input.stream.convert.to.unsigned = Convert to an unsigned byte
|
||||
inspection.trailing.whitespaces.in.text.block.name=Trailing whitespace in text block
|
||||
|
||||
Reference in New Issue
Block a user