Java: fix false positive in "Trailing whitespace in text block" inspection (IDEA-331315, IDEA-330589)

GitOrigin-RevId: 790d24c10069542bfed988ccf2eebc872ac666ff
This commit is contained in:
Bas Leijdekkers
2024-01-27 11:45:02 +00:00
committed by intellij-monorepo-bot
parent 1ed298893c
commit fca5c367cc
18 changed files with 166 additions and 93 deletions
@@ -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-2024 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.codeInsight.daemon.impl.analysis.HighlightingFeature;
@@ -8,13 +8,15 @@ import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
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.impl.source.tree.java.PsiFragmentImpl;
import com.intellij.psi.util.PsiLiteralUtil;
import com.siyeh.ig.PsiReplacementUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Function;
@@ -24,10 +26,15 @@ public final class TrailingWhitespacesInTextBlockInspection extends AbstractBase
if (!HighlightingFeature.TEXT_BLOCKS.isAvailable(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
return new JavaElementVisitor() {
@Override
public void visitFragment(@NotNull PsiFragment fragment) {
super.visitFragment(fragment);
if (!fragment.isTextBlock()) return;
checkTextBlock(fragment, (fragment.getTokenType() == JavaTokenType.TEXT_BLOCK_TEMPLATE_END) ? "\"\"\"" : "\\{");
public void visitTemplate(@NotNull PsiTemplate template) {
super.visitTemplate(template);
for (PsiFragment fragment : template.getFragments()) {
if (!fragment.isTextBlock()) return;
String suffix = (fragment.getTokenType() == JavaTokenType.TEXT_BLOCK_TEMPLATE_END) ? "\"\"\"" : "\\{";
if (checkTextBlock(fragment, suffix)) {
return;
}
}
}
@Override
@@ -37,75 +44,80 @@ public final class TrailingWhitespacesInTextBlockInspection extends AbstractBase
checkTextBlock(expression, "\"\"\"");
}
private void checkTextBlock(@NotNull PsiElement textBlock, String suffix) {
String textBlockText = textBlock.getText();
String[] lines = textBlockText.split("\n", -1);
if (lines.length < 2) return;
int indent = PsiLiteralUtil.getTextBlockIndent(lines, true, false);
if (indent == -1) return;
private boolean checkTextBlock(@NotNull PsiElement textBlock, @NotNull String suffix) {
String text = textBlock.getText();
String[] lines = text.split("\n", -1);
if (lines.length < 2) return false;
int indent = getIndent(textBlock);
if (indent == -1) return false;
int offset = 0;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (i != 0) offset++;
if (line.isBlank() || line.startsWith("\"\"\"")) {
for (int j = 3, length = line.length(); j < length; j++) {
char c = line.charAt(j);
if (!PsiLiteralUtil.isTextBlockWhiteSpace(c)) return;
}
if (i != 0) offset++; // count newline
if (line.startsWith("\"\"\"")) {
offset += line.length();
continue;
}
int lineEnd = line.endsWith(suffix)? line.length() - suffix.length() : line.length();
if (lineEnd == 0) continue;
char c = line.charAt(lineEnd - 1);
if (c == ' ' || c == '\t') {
for (int j = lineEnd - 2; j >= 0; j--) {
c = line.charAt(j);
if (c != ' ' && c != '\t') {
holder.registerProblem(textBlock, new TextRange(offset + j + 1, offset + j + 2),
JavaBundle.message("inspection.trailing.whitespaces.in.text.block.message"),
createFixes());
return;
int lineEnd;
if (line.endsWith(suffix)) {
if (suffix.equals("\\{")) return false;
lineEnd = line.length() - suffix.length();
}
else {
lineEnd = line.length();
}
boolean fragmentStart = StringUtil.startsWithChar(line, '}');
if (fragmentStart ? lineEnd != 0 : lineEnd > indent) {
char c = line.charAt(lineEnd - 1);
if (c == ' ' || c == '\t') {
for (int j = lineEnd - 2; j >= 0; j--) {
c = line.charAt(j);
if (c != ' ' && c != '\t' || j < indent && !fragmentStart) {
holder.registerProblem(textBlock, new TextRange(offset + j + 1, offset + lineEnd),
JavaBundle.message("inspection.trailing.whitespaces.in.text.block.message"),
createFixes());
return true;
}
}
}
}
offset += line.length();
}
return false;
}
};
}
private static LocalQuickFix @NotNull [] createFixes() {
return new LocalQuickFix[]{
new ReplaceTrailingWhiteSpacesFix("inspection.trailing.whitespaces.in.text.block.remove.whitespaces", c -> removeWhitespaces(c)),
new ReplaceTrailingWhiteSpacesFix("inspection.trailing.whitespaces.in.text.block.replaces.whitespaces.with.escapes",
c -> replaceWhitespacesWithEscapes(c))
new ReplaceTrailingWhiteSpacesFix(JavaBundle.message("inspection.trailing.whitespaces.in.text.block.remove.whitespaces"),
s -> removeWhitespaces(s)),
new ReplaceTrailingWhiteSpacesFix(JavaBundle.message("inspection.trailing.whitespaces.in.text.block.replaces.whitespaces.with.escapes"),
s -> replaceWhitespacesWithEscapes(s))
};
}
private static @NotNull String replaceWhitespacesWithEscapes(@NotNull TransformationContext context) {
String contentLine = context.text();
private static int getIndent(@NotNull PsiElement textBlock) {
return textBlock instanceof PsiFragment
? PsiFragmentImpl.getTextBlockFragmentIndent((PsiFragment)textBlock)
: PsiLiteralUtil.getTextBlockIndent((PsiLiteralExpression)textBlock);
}
private static @NotNull String replaceWhitespacesWithEscapes(@NotNull String contentLine) {
int len = contentLine.length();
char c = contentLine.charAt(len - 1);
return switch (c) {
return switch (contentLine.charAt(len - 1)) {
case ' ' -> contentLine.substring(0, len - 1) + "\\s";
case '\t' -> contentLine.substring(0, len - 1) + "\\t";
default -> contentLine;
};
}
private static @NotNull String removeWhitespaces(@NotNull TransformationContext context) {
String contentLine = context.text();
int j;
for (j = contentLine.length() - 2; j >= 0; j--) {
char c = contentLine.charAt(j);
if (c != ' ' && c != '\t') break;
private static @NotNull String removeWhitespaces(@NotNull String contentLine) {
for (int i = contentLine.length() - 1; i >= 0; i--) {
char c = contentLine.charAt(i);
if (c != ' ' && c != '\t') return contentLine.substring(0, i + 1);
}
String result = contentLine.substring(0, j + 1);
if (context.isEnd() && hasUnescapedLastQuote(result)) {
result = result.substring(0, result.length() - 1) + "\\\"";
}
return result;
return "";
}
public static boolean hasUnescapedLastQuote(String text) {
@@ -136,43 +148,46 @@ public final class TrailingWhitespacesInTextBlockInspection extends AbstractBase
}
private static class ReplaceTrailingWhiteSpacesFix extends PsiUpdateModCommandQuickFix {
private final String myMessage;
private final @NotNull Function<@NotNull TransformationContext, String> myTransformation;
private final @IntentionFamilyName String myMessage;
private final @NotNull Function<@NotNull String, String> myTransformation;
private ReplaceTrailingWhiteSpacesFix(@NotNull String message,
@NotNull Function<@NotNull TransformationContext, String> transformation) {
private ReplaceTrailingWhiteSpacesFix(@NotNull @IntentionFamilyName String message,
@NotNull Function<@NotNull String, String> transformation) {
myMessage = message;
myTransformation = transformation;
}
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return JavaBundle.message(myMessage);
return myMessage;
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
if (element instanceof PsiLiteralExpression expression) {
if (!expression.isTextBlock()) return;
String text = buildReplacementText(element, myTransformation);
String text = buildReplacementText(element, "\"\"\"", myTransformation);
if (text == null) return;
replaceTextBlock(expression, text);
}
else if (element instanceof PsiFragment fragment) {
if (!fragment.isTextBlock()) return;
String text = buildReplacementText(element, myTransformation);
if (text == null) return;
PsiReplacementUtil.replaceFragment(fragment, text);
if (fragment.isTextBlock() && fragment.getParent() instanceof PsiTemplate template) {
@NotNull List<@NotNull PsiFragment> fragments = template.getFragments();
for (int i = fragments.size() - 1; i >= 0; i--) {
PsiFragment current = fragments.get(i);
String suffix = fragment.getTokenType() == JavaTokenType.TEXT_BLOCK_TEMPLATE_END ? "\"\"\"" : "\\{";
String text = buildReplacementText(current, suffix, myTransformation);
if (text == null) return;
PsiReplacementUtil.replaceFragment(current, text);
if (fragment == current) break;
}
}
}
}
private static @Nullable String buildReplacementText(PsiElement element,
@NotNull Function<TransformationContext, String> lineTransformation) {
private static String buildReplacementText(PsiElement element, String suffix, Function<String, String> lineTransformation) {
String[] lines = element.getText().split("\n", -1);
String suffix =
!(element instanceof PsiFragment fragment) || fragment.getTokenType() == JavaTokenType.TEXT_BLOCK_TEMPLATE_END
? "\"\"\""
: "\\{";
int indent = getIndent(element);
StringBuilder result = new StringBuilder();
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
@@ -180,29 +195,24 @@ public final class TrailingWhitespacesInTextBlockInspection extends AbstractBase
result.append("\"\"\"\n");
continue;
}
boolean last = i == lines.length - 1;
boolean last = (i == lines.length - 1);
if (last) {
if (suffix.equals("\\{")) return result.append(line).toString();
line = line.substring(0, line.length() - suffix.length());
}
if (!isContentLineEndsWithWhitespace(line)) {
result.append(line);
String transformed = lineTransformation.apply(line);
if (transformed == null) return null;
if (last && hasUnescapedLastQuote(transformed)) {
result.append(transformed, 0, transformed.length() - 1).append("\\\"");
}
else {
CharSequence transformed = lineTransformation.apply(new TransformationContext(line, last));
if (transformed == null) return null;
else if (transformed.isEmpty()) {
result.append((line.length() < indent) ? line : " ".repeat(indent));
} else {
result.append(transformed);
}
result.append(last ? suffix : "\n");
}
return result.toString();
}
private static boolean isContentLineEndsWithWhitespace(@NotNull String line) {
if (line.isBlank()) return false;
char lastChar = line.charAt(line.length() - 1);
return lastChar == ' ' || lastChar == '\t';
}
}
private record TransformationContext(@NotNull String text, boolean isEnd) {}
}
@@ -0,0 +1,8 @@
class BadTextBlock {
void x() {
String s = <error descr="Illegal text block start: missing new line after opening quotes">"""</error>a
bad bad bad
""";
}
}
@@ -0,0 +1,9 @@
class BlankLine {
void x() {
String s = STR."""
there is something
""";
}
}
@@ -0,0 +1,9 @@
class BlankLine {
void x() {
String s = STR."""
there is something<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
""";
}
}
@@ -2,7 +2,7 @@ class Foo {
void test() {
String colors = """
red<caret>
""";
""";
}
}
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
""";
}
}
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning> """;
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>""";
}
}
@@ -1,6 +1,6 @@
class Foo {
void test() {
String text = """
hello<warning descr="Trailing whitespace characters inside text block"><caret> </warning> """;
hello<warning descr="Trailing whitespace characters inside text block"><caret> </warning>""";
}
}
@@ -0,0 +1,7 @@
class StringTemplate1 {
public void foo(String type) {
String s = STR."""
Type: \{type} of something
""";
}
}
@@ -0,0 +1,13 @@
class StringTemplate1 {
public void foo(String name, String phone, String address) {
// only one highlight, although multiple fragments with trailing whitespace
String s = STR."""
{
"name": "\{name}",
"phone": "\{phone}",
"address": "\{address}"
}
""";
}
}
@@ -0,0 +1,13 @@
class StringTemplate1 {
public void foo(String name, String phone, String address) {
// only one highlight, although multiple fragments with trailing whitespace
String s = STR."""
{
"name": "\{name}",
"phone": "\{phone}",<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
"address": "\{address}"
}
""";
}
}
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
green
""";
}
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red<caret>
red<caret> \t
green
""";
}
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
red<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
green
""";
}
@@ -1,15 +1,15 @@
class Foo {
void test() {
String colors = """
red
green
blue
@@ -1,14 +1,14 @@
class Foo {
void test() {
String colors = """
<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
red
green<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
green
@@ -1,7 +1,7 @@
class Foo {
void test() {
String colors = """
red\040\t<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
red\040\t<warning descr="Trailing whitespace characters inside text block"><caret> </warning>
green
""";
}
@@ -10,6 +10,8 @@ import org.jetbrains.annotations.Nullable;
public class TrailingWhitespacesInTextBlockInspectionTest extends LightJavaInspectionTestCase {
public void testBadTextBlock() { doTest(); }
public void testBlankLine() { doTestAll(); }
public void testEmptyLastLine() { doTestAll(); }
public void testOneLineBlock() { doTestAll(); }
public void testReplaceAtTheEnd() { doTestEscape(); }
@@ -17,8 +19,10 @@ public class TrailingWhitespacesInTextBlockInspectionTest extends LightJavaInspe
public void testStringTemplate2Java21Preview() { doTestEscape(); }
public void testStringTemplate3Java21Preview() { doTestEscape(); }
public void testStringTemplate4Java21Preview() { doTestEscape(); }
public void testStringTemplate5() { doTest(); }
public void testStringTemplate6() { doTestAll(); }
public void testTrailingSpaces() { doTestAll(); }
public void testTrailingTabs() { doTestAll(); }
public void testTrailingTabs() { doTestEscape(); }
public void testWithBlankLines() { doTestAll(); }
public void testWithEndQuotes() { doTestAll(); }
public void testWithoutTrailingSpaces() { doTest(); }