diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/StringRepeatCanBeUsedInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/StringRepeatCanBeUsedInspection.java index 03cce02babf5..897eef2dc6eb 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/StringRepeatCanBeUsedInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/StringRepeatCanBeUsedInspection.java @@ -19,6 +19,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.siyeh.ig.callMatcher.CallMatcher; import com.siyeh.ig.psiutils.*; +import com.siyeh.ipp.psiutils.ErrorUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,12 +41,14 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - if (!PsiUtil.getLanguageLevel(holder.getFile()).isAtLeast(LanguageLevel.JDK_11)) return PsiElementVisitor.EMPTY_VISITOR; + var languageLevel = PsiUtil.getLanguageLevel(holder.getFile()); + if (!languageLevel.isAtLeast(LanguageLevel.JDK_11)) return PsiElementVisitor.EMPTY_VISITOR; return new JavaElementVisitor() { @Override public void visitForStatement(@NotNull PsiForStatement statement) { PsiMethodCallExpression call = findAppendCall(statement); if (call == null) return; + if (ErrorUtil.containsDeepError(call)) return; PsiReferenceExpression qualifier = tryCast(PsiUtil.skipParenthesizedExprDown(call.getMethodExpression().getQualifierExpression()), PsiReferenceExpression.class); if (qualifier == null || !ExpressionUtil.isEffectivelyUnqualified(qualifier)) return; @@ -55,9 +58,19 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal if (var.getType().equals(PsiTypes.longType()) || VariableAccessUtils.variableIsUsed(var, call)) return; PsiExpression arg = call.getArgumentList().getExpressions()[0]; if (SideEffectChecker.mayHaveSideEffects(arg)) return; - holder.registerProblem(statement.getFirstChild(), JavaBundle.message( - "inspection.message.can.be.replaced.with.string.repeat"), - new StringRepeatCanBeUsedFix(ADD_MATH_MAX)); + if (languageLevel.isAtLeast(LanguageLevel.JDK_21)) { + PsiType type = qualifier.getType(); + if (type == null) return; + String builderClassName = type.getPresentableText(); + holder.registerProblem(statement.getFirstChild(), + JavaBundle.message("inspection.message.can.be.replaced.with.builder.repeat", + builderClassName + ".repeat()"), + new AbstractStringBuilderRepeatCanBeUsedFix(ADD_MATH_MAX, builderClassName)); + } + else { + holder.registerProblem(statement.getFirstChild(), JavaBundle.message("inspection.message.can.be.replaced.with.string.repeat"), + new StringRepeatCanBeUsedFix(ADD_MATH_MAX)); + } } }; } @@ -70,11 +83,40 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal return call; } - private static final class StringRepeatCanBeUsedFix extends PsiUpdateModCommandQuickFix { - private final boolean myAddMathMax; + private static final class AbstractStringBuilderRepeatCanBeUsedFix extends RepeatCanBeUsedFix { + private final String builderClassShortName; + private AbstractStringBuilderRepeatCanBeUsedFix(boolean addMathMax, String builderClassShortName) { + super(addMathMax); + this.builderClassShortName = builderClassShortName; + } + + @Override + public @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull String getFamilyName() { + return CommonQuickFixBundle.message("fix.replace.with.x", builderClassShortName + ".repeat()"); + } + + @Override + protected void replaceWithRepeatCall(PsiExpression qualifierExpression, + String repeatedStringExpression, + String countText, + CommentTracker ct, + PsiExpression arg, + PsiForStatement forStatement, + PsiMethodCallExpression appendCall) { + String replacement = qualifierExpression.getText() + ".repeat(" + repeatedStringExpression + ", " + countText + ");"; + PsiExpressionStatement result = (PsiExpressionStatement)ct.replaceAndRestoreComments(forStatement, replacement); + if (myAddMathMax) { + PsiMethodCallExpression repeatCall = (PsiMethodCallExpression)result.getExpression(); + PsiMethodCallExpression maxCall = (PsiMethodCallExpression)repeatCall.getArgumentList().getExpressions()[1]; + simplifyMaxCall(maxCall); + } + } + } + + private static final class StringRepeatCanBeUsedFix extends RepeatCanBeUsedFix { private StringRepeatCanBeUsedFix(boolean addMathMax) { - myAddMathMax = addMathMax; + super(addMathMax); } @Override @@ -82,17 +124,45 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal return CommonQuickFixBundle.message("fix.replace.with.x", "String.repeat()"); } + @Override + protected void replaceWithRepeatCall(PsiExpression qualifierExpression, + String repeatedStringExpression, + String countText, + CommentTracker ct, + PsiExpression arg, + PsiForStatement forStatement, + PsiMethodCallExpression call) { + String replacement = repeatedStringExpression + ".repeat(" + countText + ")"; + ct.replace(arg, replacement); + PsiExpressionStatement result = (PsiExpressionStatement)ct.replaceAndRestoreComments(forStatement, call.getParent()); + if (myAddMathMax) { + PsiMethodCallExpression appendCall = (PsiMethodCallExpression)result.getExpression(); + PsiMethodCallExpression repeatCall = (PsiMethodCallExpression)appendCall.getArgumentList().getExpressions()[0]; + PsiMethodCallExpression maxCall = (PsiMethodCallExpression)repeatCall.getArgumentList().getExpressions()[0]; + simplifyMaxCall(maxCall); + } + } + } + + private static abstract class RepeatCanBeUsedFix extends PsiUpdateModCommandQuickFix { + protected final boolean myAddMathMax; + + private RepeatCanBeUsedFix(boolean addMathMax) { + myAddMathMax = addMathMax; + } + @Override protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) { - PsiForStatement statement = PsiTreeUtil.getParentOfType(element, PsiForStatement.class); - if (statement == null) return; - CountingLoop loop = CountingLoop.from(statement); + PsiForStatement forStatement = PsiTreeUtil.getParentOfType(element, PsiForStatement.class); + if (forStatement == null) return; + CountingLoop loop = CountingLoop.from(forStatement); if (loop == null) return; - PsiMethodCallExpression call = findAppendCall(statement); - if (call == null) return; - PsiExpression builder = call.getMethodExpression().getQualifierExpression(); - if (builder == null) return; - PsiExpression arg = call.getArgumentList().getExpressions()[0]; + PsiMethodCallExpression appendCall = findAppendCall(forStatement); + if (appendCall == null) return; + if (ErrorUtil.containsDeepError(appendCall)) return; + PsiExpression qualifierExpression = appendCall.getMethodExpression().getQualifierExpression(); + if (qualifierExpression == null) return; + PsiExpression arg = appendCall.getArgumentList().getExpressions()[0]; PsiExpression from, to; if (loop.isDescending()) { from = loop.getBound(); @@ -103,23 +173,28 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal to = loop.getBound(); } CommentTracker ct = new CommentTracker(); - String repeatQualifier = getRepeatQualifier(arg, ct); + String repeatedStringExpression = getRepeatedStringExpression(arg, ct); String countText = getCountText(from, to, loop.isIncluding(), ct); if (myAddMathMax) { countText = CommonClassNames.JAVA_LANG_MATH + ".max(0," + countText + ")"; } - String replacement = repeatQualifier + ".repeat(" + countText + ")"; - ct.replace(arg, replacement); - PsiExpressionStatement result = (PsiExpressionStatement)ct.replaceAndRestoreComments(statement, call.getParent()); - if (myAddMathMax) { - PsiMethodCallExpression appendCall = (PsiMethodCallExpression)result.getExpression(); - PsiMethodCallExpression repeatCall = (PsiMethodCallExpression)appendCall.getArgumentList().getExpressions()[0]; - PsiMethodCallExpression maxCall = (PsiMethodCallExpression)repeatCall.getArgumentList().getExpressions()[0]; - PsiExpression count = maxCall.getArgumentList().getExpressions()[1]; - LongRangeSet range = CommonDataflow.getExpressionRange(count); - if (range != null && !range.isEmpty() && range.min() >= 0) { - maxCall.replace(count); - } + replaceWithRepeatCall(qualifierExpression, repeatedStringExpression, countText, ct, arg, forStatement, appendCall); + } + + protected abstract void replaceWithRepeatCall( + PsiExpression qualifierExpression, + String repeatedStringExpression, + String countText, + CommentTracker ct, + PsiExpression arg, + PsiForStatement forStatement, + PsiMethodCallExpression appendCall); + + protected void simplifyMaxCall(PsiMethodCallExpression maxCall) { + PsiExpression count = maxCall.getArgumentList().getExpressions()[1]; + LongRangeSet range = CommonDataflow.getExpressionRange(count); + if (range != null && !range.isEmpty() && range.min() >= 0) { + maxCall.replace(count); } } @@ -146,15 +221,16 @@ public final class StringRepeatCanBeUsedInspection extends AbstractBaseJavaLocal return countText; } - private static @NotNull String getRepeatQualifier(PsiExpression arg, CommentTracker ct) { - if (arg instanceof PsiLiteralExpression literal && !TypeUtils.isJavaLangString(arg.getType())) { + private static @NotNull String getRepeatedStringExpression(PsiExpression arg, CommentTracker ct) { + boolean isStringType = TypeUtils.isJavaLangString(arg.getType()); + if (arg instanceof PsiLiteralExpression literal && !isStringType) { Object value = literal.getValue(); if (value instanceof Character) { return PsiLiteralUtil.stringForCharLiteral(literal.getText()); } return StringUtil.wrapWithDoubleQuote(StringUtil.escapeStringCharacters(String.valueOf(value))); } - if (TypeUtils.isJavaLangString(arg.getType()) && NullabilityUtil.getExpressionNullability(arg, true) == Nullability.NOT_NULL) { + if (isStringType && NullabilityUtil.getExpressionNullability(arg, true) == Nullability.NOT_NULL) { return ct.text(arg, ParenthesesUtils.METHOD_CALL_PRECEDENCE); } return CommonClassNames.JAVA_LANG_STRING + ".valueOf(" + ct.text(arg) + ")"; diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index dc2874064c53..5c68d92c938d 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -1859,10 +1859,10 @@ groupKey="group.names.language.level.specific.issues.and.migration.aids11" enabledByDefault="true" level="WARNING" implementationClass="com.intellij.codeInspection.ReadWriteStringCanBeUsedInspection" key="inspection.read.write.string.can.be.used.display.name" bundle="messages.JavaBundle"/> - -Reports loops that can be replaced with a single String.repeat() method (available since Java 11). -

Example:

+Reports loops that can be replaced with a single StringBuilder.repeat() method (available since Java 21) or +String.repeat() method (available since Java 11). +

Example (Java 11):


   void append(StringBuilder sb, int count, Object obj) {
     for (int i = 0; i < count; i++) {
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeat.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeat.java
new file mode 100644
index 000000000000..09ab97f6c6f0
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeat.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    // comment before for-loop
+      // comment before append
+      sb.repeat(" ", 100);
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiff.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiff.java
new file mode 100644
index 000000000000..488a330565f6
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiff.java
@@ -0,0 +1,8 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String spaces(int a, int b, int c, int d) {
+    StringBuilder sb = new StringBuilder();
+      sb.repeat(" ", Math.max(0, c - d - (a - b)));
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiffKnown.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiffKnown.java
new file mode 100644
index 000000000000..c6ae1b4e0c96
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatDiffKnown.java
@@ -0,0 +1,8 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String testRepeat(String s, StringBuilder sb, int digits) {
+    if ((s.length() < digits) && (sb.length() > 0)) {
+        sb.repeat("0", digits - s.length());
+    }
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatField.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatField.java
new file mode 100644
index 000000000000..3c3df75756c8
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatField.java
@@ -0,0 +1,11 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  public int pendingSpaces;
+  
+  String testRepeat(StringBuilder buffer) {
+    if (pendingSpaces > 0) {
+        buffer.repeat(" ", pendingSpaces);
+      pendingSpaces = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatInclusive.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatInclusive.java
new file mode 100644
index 000000000000..eae1e8d1e3c2
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatInclusive.java
@@ -0,0 +1,8 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String spaces(int c, int d) {
+    StringBuilder sb = new StringBuilder();
+      sb.repeat("123456", Math.max(0, c - d));
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstant.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstant.java
new file mode 100644
index 000000000000..119f55c686fc
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstant.java
@@ -0,0 +1,8 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String hundredTimes(String s) {
+    StringBuilder sb = new StringBuilder();
+      sb.repeat(String.valueOf(s), 100);
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstantNullChecked.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstantNullChecked.java
new file mode 100644
index 000000000000..db07583c49c9
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterRepeatNonConstantNullChecked.java
@@ -0,0 +1,11 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+import java.util.Objects;
+
+class Test {
+  String hundredTimes(String s) {
+    Objects.requireNonNull(s);
+    StringBuilder sb = new StringBuilder();
+      sb.repeat(s, 100);
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/afterStringBuffer.java b/java/java-tests/testData/inspection/stringBuilderRepeat/afterStringBuffer.java
new file mode 100644
index 000000000000..4e1aec00d727
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/afterStringBuffer.java
@@ -0,0 +1,8 @@
+// "Replace with 'StringBuffer.repeat()'" "true"
+class Test {
+  String hundredSpaces() {
+    StringBuffer sb = new StringBuffer();
+      sb.repeat(" ", 100);
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenChar.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenChar.java
new file mode 100644
index 000000000000..07c22fa1e42b
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenChar.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "false"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append('*
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenParenthesizedString.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenParenthesizedString.java
new file mode 100644
index 000000000000..990f4dd14aaf
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenParenthesizedString.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "false"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append(("*
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenString.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenString.java
new file mode 100644
index 000000000000..1ab4bb1da5db
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeBrokenString.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "false"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append("a" + "*
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeat.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeat.java
new file mode 100644
index 000000000000..6d2c007a5dd9
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeat.java
@@ -0,0 +1,12 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    // comment before for-loop
+    for(int i=0; i<100; i++) {
+      // comment before append
+      sb.append(" ");
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatDiff.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatDiff.java
new file mode 100644
index 000000000000..0b5141146a51
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatDiff.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String spaces(int a, int b, int c, int d) {
+    StringBuilder sb = new StringBuilder();
+    for(int i=a-b; i 0)) {
+      for (int i=s.length(); i < digits; i++) {
+        sb.append('0');
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatField.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatField.java
new file mode 100644
index 000000000000..814f49a6e4d2
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatField.java
@@ -0,0 +1,12 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  public int pendingSpaces;
+  
+  String testRepeat(StringBuilder buffer) {
+    if (pendingSpaces > 0) {
+      for (int sp = 0; sp < pendingSpaces; sp++)
+        buffer.append(' ');
+      pendingSpaces = 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatInclusive.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatInclusive.java
new file mode 100644
index 000000000000..41588092bfbe
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatInclusive.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String spaces(int c, int d) {
+    StringBuilder sb = new StringBuilder();
+    for(int i=1; i<=c-d; i++) {
+      sb.append(123_456);
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstant.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstant.java
new file mode 100644
index 000000000000..7e1c98cbebe4
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstant.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+class Test {
+  String hundredTimes(String s) {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append(s);
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstantNullChecked.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstantNullChecked.java
new file mode 100644
index 000000000000..d554eb7432c0
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatNonConstantNullChecked.java
@@ -0,0 +1,13 @@
+// "Replace with 'StringBuilder.repeat()'" "true"
+import java.util.Objects;
+
+class Test {
+  String hundredTimes(String s) {
+    Objects.requireNonNull(s);
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append(s);
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatSideEffect.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatSideEffect.java
new file mode 100644
index 000000000000..d419d99e537a
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeRepeatSideEffect.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuilder.repeat()'" "false"
+class Test {
+  String hundredNumbers() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append(Math.random());
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringBuilderRepeat/beforeStringBuffer.java b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeStringBuffer.java
new file mode 100644
index 000000000000..a6c4ff8a8de3
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringBuilderRepeat/beforeStringBuffer.java
@@ -0,0 +1,10 @@
+// "Replace with 'StringBuffer.repeat()'" "true"
+class Test {
+  String hundredSpaces() {
+    StringBuffer sb = new StringBuffer();
+    for(int i=0; i<100; i++) {
+      sb.append(" ");
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringRepeat/beforeBrokenChar.java b/java/java-tests/testData/inspection/stringRepeat/beforeBrokenChar.java
new file mode 100644
index 000000000000..39924642f9cb
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringRepeat/beforeBrokenChar.java
@@ -0,0 +1,10 @@
+// "Replace with 'String.repeat()'" "false"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append('*
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/inspection/stringRepeat/beforeBrokenString.java b/java/java-tests/testData/inspection/stringRepeat/beforeBrokenString.java
new file mode 100644
index 000000000000..ef308170474d
--- /dev/null
+++ b/java/java-tests/testData/inspection/stringRepeat/beforeBrokenString.java
@@ -0,0 +1,10 @@
+// "Replace with 'String.repeat()'" "false"
+class Test {
+  String hundredSpaces() {
+    StringBuilder sb = new StringBuilder();
+    for(int i=0; i<100; i++) {
+      sb.append("a" + "*
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/StringBuilderRepeatCanBeUsedInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/StringBuilderRepeatCanBeUsedInspectionTest.java
new file mode 100644
index 000000000000..4db3d3c3c587
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/StringBuilderRepeatCanBeUsedInspectionTest.java
@@ -0,0 +1,28 @@
+// Copyright 2000-2019 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.LocalInspectionTool;
+import com.intellij.codeInspection.StringRepeatCanBeUsedInspection;
+import com.intellij.testFramework.LightProjectDescriptor;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase.JAVA_21;
+
+public class StringBuilderRepeatCanBeUsedInspectionTest extends LightQuickFixParameterizedTestCase {
+  @Override
+  protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
+    return new LocalInspectionTool[]{new StringRepeatCanBeUsedInspection()};
+  }
+
+  @NotNull
+  @Override
+  protected LightProjectDescriptor getProjectDescriptor() {
+    return JAVA_21;
+  }
+
+  @Override
+  protected String getBasePath() {
+    return "/inspection/stringBuilderRepeat";
+  }
+}
diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties
index afbcfd29353a..375d5fa31bfd 100644
--- a/java/openapi/resources/messages/JavaBundle.properties
+++ b/java/openapi/resources/messages/JavaBundle.properties
@@ -531,6 +531,7 @@ inspection.message.can.be.replaced.with.files.readstring=Can be replaced with 'F
 inspection.message.can.be.replaced.with.optional.of.nullable=Can be replaced with Optional.ofNullable()
 inspection.message.can.be.replaced.with.single.expression.in.functional.style=Can be replaced with single expression in functional style
 inspection.message.can.be.replaced.with.string.repeat=Can be replaced with 'String.repeat()'
+inspection.message.can.be.replaced.with.builder.repeat=Can be replaced with ''{0}''
 inspection.message.lambda.parameter.type.is.redundant=Lambda parameter type is redundant
 inspection.message.pseudo.functional.style.code=Pseudo functional style code
 inspection.message.redundant.default.parameter.value.assignment=Redundant default parameter value assignment
@@ -1410,7 +1411,7 @@ inspection.explicit.array.filling.display.name=Explicit array filling
 inspection.java.8.collection.remove.if.display.name=Loop can be replaced with 'Collection.removeIf()'
 inspection.java.8.list.replace.all.display.name=Loop can be replaced with 'List.replaceAll()'
 inspection.java.8.map.api.display.name=Simplifiable 'Map' operations
-inspection.string.repeat.can.be.used.display.name=String.repeat() can be used
+inspection.string.repeat.can.be.used.display.name='repeat()' method can be used
 inspection.read.write.string.can.be.used.display.name='Files.readString()' or 'Files.writeString()' can be used
 inspection.java.9.collection.factory.display.name=Immutable collection creation can be replaced with collection factory call
 inspection.explicit.argument.can.be.lambda.display.name=Explicit argument can be lambda