From fca5c367cc32861d857b286ec7ef57e062e046ed Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Fri, 26 Jan 2024 12:47:26 +0100 Subject: [PATCH] Java: fix false positive in "Trailing whitespace in text block" inspection (IDEA-331315, IDEA-330589) GitOrigin-RevId: 790d24c10069542bfed988ccf2eebc872ac666ff --- ...ilingWhitespacesInTextBlockInspection.java | 164 ++++++++++-------- .../BadTextBlock.java | 8 + .../BlankLine.after.java | 9 + .../BlankLine.java | 9 + .../EmptyLastLine.after.java | 2 +- .../EmptyLastLine.java | 2 +- .../OneLineBlock.java | 2 +- .../ReplaceAtTheEnd.java | 2 +- .../StringTemplate5.java | 7 + .../StringTemplate6.after.java | 13 ++ .../StringTemplate6.java | 13 ++ .../TrailingSpaces.java | 2 +- .../TrailingTabs.after.java | 2 +- .../TrailingTabs.java | 2 +- .../WithBlankLines.after.java | 10 +- .../WithBlankLines.java | 4 +- .../WithUnicodeEscapes.java | 2 +- ...gWhitespacesInTextBlockInspectionTest.java | 6 +- 18 files changed, 166 insertions(+), 93 deletions(-) create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BadTextBlock.java create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.after.java create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.java create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate5.java create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.after.java create mode 100644 java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.java diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/TrailingWhitespacesInTextBlockInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/TrailingWhitespacesInTextBlockInspection.java index 32dd1fc58c6c..58b2363955cc 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/TrailingWhitespacesInTextBlockInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/TrailingWhitespacesInTextBlockInspection.java @@ -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 lineTransformation) { + private static String buildReplacementText(PsiElement element, String suffix, Function 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) {} } \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BadTextBlock.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BadTextBlock.java new file mode 100644 index 000000000000..da22c90fd544 --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BadTextBlock.java @@ -0,0 +1,8 @@ +class BadTextBlock { + + void x() { + String s = """a + bad bad bad + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.after.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.after.java new file mode 100644 index 000000000000..1361c4131b27 --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.after.java @@ -0,0 +1,9 @@ +class BlankLine { + + void x() { + String s = STR.""" + + there is something + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.java new file mode 100644 index 000000000000..2f14b698876d --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/BlankLine.java @@ -0,0 +1,9 @@ +class BlankLine { + + void x() { + String s = STR.""" + + there is something + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.after.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.after.java index 9ce71fc1c76f..d30bd8319c3f 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.after.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.after.java @@ -2,7 +2,7 @@ class Foo { void test() { String colors = """ red - """; + """; } } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.java index 91474f3ae625..ea30f12aaf0c 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/EmptyLastLine.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red + red """; } } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/OneLineBlock.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/OneLineBlock.java index 33a5b8f9f2fe..5b5250799d56 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/OneLineBlock.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/OneLineBlock.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red """; + red """; } } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/ReplaceAtTheEnd.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/ReplaceAtTheEnd.java index 4b47a73602e9..a9ec7d64478e 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/ReplaceAtTheEnd.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/ReplaceAtTheEnd.java @@ -1,6 +1,6 @@ class Foo { void test() { String text = """ - hello """; + hello """; } } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate5.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate5.java new file mode 100644 index 000000000000..5744171f2404 --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate5.java @@ -0,0 +1,7 @@ +class StringTemplate1 { + public void foo(String type) { + String s = STR.""" + Type: \{type} of something + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.after.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.after.java new file mode 100644 index 000000000000..8a3b2d91ea31 --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.after.java @@ -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}" + } + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.java new file mode 100644 index 000000000000..f748b88533b2 --- /dev/null +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/StringTemplate6.java @@ -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}" + } + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingSpaces.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingSpaces.java index 5139734fffab..b1ce1bdf71c3 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingSpaces.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingSpaces.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red + red green """; } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.after.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.after.java index 1515845aa9c4..846adb395f1d 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.after.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.after.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red + red \t green """; } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.java index 8b6fd9c7387e..659a0928cb54 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/TrailingTabs.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red + red green """; } diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.after.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.after.java index 71833d4ae922..c329434fb51c 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.after.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.after.java @@ -1,15 +1,15 @@ class Foo { void test() { String colors = """ - - + + red - - + + green - + blue diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.java index cd03af45e8b1..2a0330674eac 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithBlankLines.java @@ -1,14 +1,14 @@ class Foo { void test() { String colors = """ - + red - green + green diff --git a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithUnicodeEscapes.java b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithUnicodeEscapes.java index 8be2c8fd6df5..80455dccc305 100644 --- a/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithUnicodeEscapes.java +++ b/java/java-tests/testData/inspection/trailingWhitespacesInTextBlock/WithUnicodeEscapes.java @@ -1,7 +1,7 @@ class Foo { void test() { String colors = """ - red\040\t + red\040\t green """; } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/TrailingWhitespacesInTextBlockInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/TrailingWhitespacesInTextBlockInspectionTest.java index 1231a69afbd9..261f0f2c114a 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/TrailingWhitespacesInTextBlockInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/TrailingWhitespacesInTextBlockInspectionTest.java @@ -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(); }