diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConcatenationToMessageFormatAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConcatenationToMessageFormatAction.java index db7b5e254cb2..6e20bff6e4f7 100644 --- a/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConcatenationToMessageFormatAction.java +++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConcatenationToMessageFormatAction.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// 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.codeInsight.intention.impl; import com.intellij.codeInsight.AnnotationUtil; @@ -20,10 +6,12 @@ import com.intellij.codeInsight.CodeInsightBundle; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.codeStyle.JavaCodeStyleManager; +import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl; import com.intellij.psi.util.PsiConcatenationUtil; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; @@ -33,6 +21,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -56,15 +45,20 @@ public class ConcatenationToMessageFormatAction implements IntentionAction { final PsiElement element = findElementAtCaret(editor, file); PsiPolyadicExpression concatenation = getEnclosingLiteralConcatenation(element); if (concatenation == null) return; - StringBuilder formatString = new StringBuilder(); List args = new ArrayList<>(); - PsiConcatenationUtil.buildFormatString(concatenation, formatString, args, false); + final String formatString = PsiConcatenationUtil.buildFormatString(concatenation, false, args); final PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); PsiMethodCallExpression call = (PsiMethodCallExpression) factory.createExpressionFromText("java.text.MessageFormat.format()", concatenation); PsiExpressionList argumentList = call.getArgumentList(); - PsiExpression formatArgument = factory.createExpressionFromText("\"" + formatString.toString() + "\"", null); + boolean textBlocks = Arrays.stream(concatenation.getOperands()) + .anyMatch(operand -> operand instanceof PsiLiteralExpressionImpl && + ((PsiLiteralExpressionImpl)operand).getLiteralElementType() == JavaTokenType.TEXT_BLOCK_LITERAL); + final String expressionText = textBlocks + ? "\"\"\"\n" + StringUtil.escapeTextBlockCharacters(formatString) + "\"\"\"" + : "\"" + StringUtil.escapeStringCharacters(formatString) + "\""; + PsiExpression formatArgument = factory.createExpressionFromText(expressionText, null); argumentList.add(formatArgument); if (PsiUtil.isLanguageLevel5OrHigher(file)) { for (PsiExpression arg : args) { diff --git a/java/java-psi-api/src/com/intellij/psi/util/PsiConcatenationUtil.java b/java/java-psi-api/src/com/intellij/psi/util/PsiConcatenationUtil.java index af2317d2b678..2128b7d6c118 100644 --- a/java/java-psi-api/src/com/intellij/psi/util/PsiConcatenationUtil.java +++ b/java/java-psi-api/src/com/intellij/psi/util/PsiConcatenationUtil.java @@ -1,21 +1,6 @@ -/* - * Copyright 2000-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// 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.psi.util; -import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ObjectUtils; @@ -28,17 +13,29 @@ import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING; public class PsiConcatenationUtil { + /** + * @param formatParameters output parameter, will contain the format parameters found in the concatenation + * @return unescaped (!) format string produced from the concatenation + */ + public static String buildFormatString(PsiExpression concatenation, boolean printfFormat, + List formatParameters) { + final StringBuilder result = new StringBuilder(); + buildFormatString(concatenation, result, formatParameters, printfFormat); + return result.toString(); + } + + // externally used public static void buildFormatString(PsiExpression expression, StringBuilder formatString, List formatParameters, boolean printfFormat) { if (expression instanceof PsiLiteralExpression) { final PsiLiteralExpression literalExpression = (PsiLiteralExpression) expression; - TextRange range = ElementManipulators.getValueTextRange(expression); - String formatText = range.substring(literalExpression.getText()); + final String text = String.valueOf(literalExpression.getValue()); + String formatText; if (printfFormat) { - formatText = formatText.replace("%", "%%").replace("\\'", "'"); + formatText = text.replace("%", "%%").replace("\\'", "'"); } else { - formatText = formatText.replace("'", "''").replaceAll("((\\{|})+)", "'$1'"); + formatText = text.replace("'", "''").replaceAll("([{}]+)", "'$1'"); } formatString.append(formatText); } else if (expression instanceof PsiPolyadicExpression) { @@ -46,18 +43,21 @@ public class PsiConcatenationUtil { if (type != null && type.equalsToText(JAVA_LANG_STRING)) { final PsiPolyadicExpression binaryExpression = (PsiPolyadicExpression) expression; PsiExpression[] operands = binaryExpression.getOperands(); - PsiType left = operands[0].getType(); - boolean stringStarted = left != null && left.equalsToText(JAVA_LANG_STRING); + PsiType first = operands[0].getType(); + PsiType second = operands[1].getType(); + boolean stringStarted = first != null && first.equalsToText(JAVA_LANG_STRING) || + second != null && second.equalsToText(JAVA_LANG_STRING); if (stringStarted) { buildFormatString(operands[0], formatString, formatParameters, printfFormat); } for (int i = 1; i < operands.length; i++) { PsiExpression op = operands[i]; PsiType optype = op.getType(); - PsiType r = TypeConversionUtil.calcTypeForBinaryExpression(left, optype, binaryExpression.getOperationTokenType(), true); + PsiType r = TypeConversionUtil.calcTypeForBinaryExpression(first, optype, binaryExpression.getOperationTokenType(), true); if (r != null && r.equalsToText(JAVA_LANG_STRING) && !stringStarted) { stringStarted = true; PsiElement element = binaryExpression.getTokenBeforeOperand(op); + assert element != null; if (element.getPrevSibling() instanceof PsiWhiteSpace) element = element.getPrevSibling(); String text = binaryExpression.getText().substring(0, element.getStartOffsetInParent()); PsiExpression subExpression = JavaPsiFacade.getElementFactory(binaryExpression.getProject()) @@ -72,7 +72,7 @@ public class PsiConcatenationUtil { addFormatParameter(op, formatString, formatParameters, printfFormat); } } - left = r; + first = r; } } else { diff --git a/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.after.java b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.after.java new file mode 100644 index 000000000000..f46f567c8452 --- /dev/null +++ b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.after.java @@ -0,0 +1,5 @@ +class C { + private static String quote(String s) { + return java.text.MessageFormat.format("\"{0}\"", s); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.java b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.java new file mode 100644 index 000000000000..429e48d4b7d0 --- /dev/null +++ b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/escaping.java @@ -0,0 +1,5 @@ +class C { + private static String quote(String s) { + return '"' + s + '"'; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.after.java b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.after.java new file mode 100644 index 000000000000..27debc3ff585 --- /dev/null +++ b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.after.java @@ -0,0 +1,11 @@ +class C { + + void x(int a, int b) { + //keep me + String s = java.text.MessageFormat.format(""" +the text + block +line2 +{0}{1} "to" be""", a, b); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.java b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.java new file mode 100644 index 000000000000..e98dcffcdbdc --- /dev/null +++ b/java/java-tests/testData/codeInsight/concatenationToMessageFormat/textblock.java @@ -0,0 +1,11 @@ +class C { + + void x(int a, int b) { + String s = """ + the text\n block + line2 + """ + + a + b + //keep me + " \"to\" be"; + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConcatenationToMessageFormatTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConcatenationToMessageFormatTest.java index 899c5fd85603..67634a6a21d9 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConcatenationToMessageFormatTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConcatenationToMessageFormatTest.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// 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.codeInsight.intention; import com.intellij.JavaTestUtil; @@ -39,6 +25,8 @@ public class ConcatenationToMessageFormatTest extends JavaCodeInsightFixtureTest } public void testComments() { doTest(); } + public void testTextblock() { doTest(); } + public void testEscaping() { doTest(); } private void doTest() { final String name = getTestName(true); @@ -53,14 +41,12 @@ public class ConcatenationToMessageFormatTest extends JavaCodeInsightFixtureTest private void doTest(String expressionText, String messageFormatText, String... foundExpressionTexts) { final PsiExpression expression = getElementFactory().createExpressionFromText(expressionText, null); - final StringBuilder result = new StringBuilder(); final ArrayList args = new ArrayList<>(); - PsiConcatenationUtil.buildFormatString(expression, result, args, false); - assertEquals(messageFormatText, result.toString()); + final String formatString = PsiConcatenationUtil.buildFormatString(expression, false, args); + assertEquals(messageFormatText, formatString); assertEquals(foundExpressionTexts.length, args.size()); for (int i = 0; i < foundExpressionTexts.length; i++) { - final String foundExpressionText = foundExpressionTexts[i]; - assertEquals(foundExpressionText, args.get(i).getText()); + assertEquals(foundExpressionTexts[i], args.get(i).getText()); } } @@ -69,7 +55,7 @@ public class ConcatenationToMessageFormatTest extends JavaCodeInsightFixtureTest } public void test2() { - doTest("1 + 2 + 3 + \"{}'\" + '\\n' + ((java.lang.String)ccc)", "{0}'{}'''\\n{1}", "1 + 2 + 3", "ccc"); + doTest("1 + 2 + 3 + \"{}'\" + '\\n' + ((java.lang.String)ccc)", "{0}'{}'''\n{1}", "1 + 2 + 3", "ccc"); } public void test3() { diff --git a/plugins/IntentionPowerPak/src/com/siyeh/ipp/concatenation/ReplaceConcatenationWithFormatStringIntention.java b/plugins/IntentionPowerPak/src/com/siyeh/ipp/concatenation/ReplaceConcatenationWithFormatStringIntention.java index 7ac402a80ac8..92a6eb6f456b 100644 --- a/plugins/IntentionPowerPak/src/com/siyeh/ipp/concatenation/ReplaceConcatenationWithFormatStringIntention.java +++ b/plugins/IntentionPowerPak/src/com/siyeh/ipp/concatenation/ReplaceConcatenationWithFormatStringIntention.java @@ -1,20 +1,7 @@ -/* - * Copyright 2008-2018 Bas Leijdekkers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// 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.siyeh.ipp.concatenation; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl; import com.intellij.psi.util.PsiConcatenationUtil; @@ -45,9 +32,8 @@ public class ReplaceConcatenationWithFormatStringIntention extends Intention { expression = (PsiPolyadicExpression)parent; parent = expression.getParent(); } - final StringBuilder formatString = new StringBuilder(); final List formatParameters = new ArrayList<>(); - PsiConcatenationUtil.buildFormatString(expression, formatString, formatParameters, true); + final String formatString = PsiConcatenationUtil.buildFormatString(expression, true, formatParameters); if (replaceWithPrintfExpression(expression, formatString, formatParameters)) { return; } @@ -96,7 +82,7 @@ public class ReplaceConcatenationWithFormatStringIntention extends Intention { } final String qualifiedName = containingClass.getQualifiedName(); if (!"java.io.PrintStream".equals(qualifiedName) && - !"java.io.Printwriter".equals(qualifiedName)) { + !"java.io.PrintWriter".equals(qualifiedName)) { return false; } CommentTracker commentTracker = new CommentTracker(); @@ -106,9 +92,9 @@ public class ReplaceConcatenationWithFormatStringIntention extends Intention { newExpression.append(commentTracker.text(qualifier)).append('.'); } newExpression.append("printf("); - appendFormatString(expression, formatString, insertNewline, newExpression); + appendFormatString(expression, formatString.toString(), insertNewline, newExpression); for (PsiExpression formatParameter : formatParameters) { - newExpression.append(", ").append(commentTracker.text(formatParameter)); + newExpression.append(",").append(commentTracker.text(formatParameter)); } newExpression.append(')'); PsiReplacementUtil.replaceExpression(methodCallExpression, newExpression.toString(), commentTracker); @@ -116,17 +102,26 @@ public class ReplaceConcatenationWithFormatStringIntention extends Intention { } private static void appendFormatString(PsiPolyadicExpression expression, - CharSequence formatString, + String formatString, boolean insertNewline, StringBuilder newExpression) { boolean textBlocks = Arrays.stream(expression.getOperands()) .anyMatch(operand -> operand instanceof PsiLiteralExpressionImpl && ((PsiLiteralExpressionImpl)operand).getLiteralElementType() == JavaTokenType.TEXT_BLOCK_LITERAL); - newExpression.append(textBlocks ? "\"\"\"\n" : '\"'); - newExpression.append(formatString); - if (insertNewline) { - newExpression.append("%n"); + if (textBlocks) { + newExpression.append("\"\"\"\n"); + newExpression.append(StringUtil.escapeTextBlockCharacters(formatString)); + if (insertNewline) { + newExpression.append('\n'); + } + newExpression.append("\"\"\""); + } else { + newExpression.append('\"'); + newExpression.append(StringUtil.escapeStringCharacters(formatString)); + if (insertNewline) { + newExpression.append("%n"); + } + newExpression.append('\"'); } - newExpression.append(textBlocks ? "\"\"\"" : '\"'); } } diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/concatenation/string_format/PreserveTextBlock_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/concatenation/string_format/PreserveTextBlock_after.java index aa44efe85220..9a06f822d781 100644 --- a/plugins/IntentionPowerPak/test/com/siyeh/ipp/concatenation/string_format/PreserveTextBlock_after.java +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/concatenation/string_format/PreserveTextBlock_after.java @@ -1,7 +1,8 @@ class C { //keep me String s = String.format(""" - the text\n block - line2 - %d%d to be""", 1, 2); + the text + block + line2 +%d%d to be""", 1, 2); } \ No newline at end of file diff --git a/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nizeConcatenationQuickFix.java b/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nizeConcatenationQuickFix.java index 31a6d17eabc3..05b0a43b384e 100644 --- a/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nizeConcatenationQuickFix.java +++ b/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nizeConcatenationQuickFix.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// 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.codeInspection.i18n; import com.intellij.codeInsight.CodeInsightBundle; @@ -21,6 +7,7 @@ import com.intellij.lang.properties.psi.PropertiesFile; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.util.PsiConcatenationUtil; import com.intellij.psi.util.PsiTreeUtil; @@ -49,6 +36,7 @@ public class I18nizeConcatenationQuickFix extends I18nizeQuickFix{ @Override public JavaI18nizeQuickFixDialog createDialog(Project project, Editor editor, PsiFile psiFile) { PsiPolyadicExpression concatenation = getEnclosingLiteralConcatenation(psiFile, editor); + assert concatenation != null; PsiLiteralExpression literalExpression = getContainingLiteral(concatenation); if (literalExpression == null) return null; return createDialog(project, psiFile, literalExpression); @@ -66,6 +54,7 @@ public class I18nizeConcatenationQuickFix extends I18nizeQuickFix{ @Nullable PsiLiteralExpression literalExpression, String i18nizedText) throws IncorrectOperationException { PsiPolyadicExpression concatenation = getEnclosingLiteralConcatenation(psiFile, editor); + assert concatenation != null; PsiExpression expression = JavaPsiFacade.getInstance(psiFile.getProject()).getElementFactory().createExpressionFromText(i18nizedText, concatenation); return concatenation.replace(expression); } @@ -77,16 +66,16 @@ public class I18nizeConcatenationQuickFix extends I18nizeQuickFix{ @Override protected JavaI18nizeQuickFixDialog createDialog(final Project project, final PsiFile context, final PsiLiteralExpression literalExpression) { PsiPolyadicExpression concatenation = getEnclosingLiteralConcatenation(literalExpression); - StringBuilder formatString = new StringBuilder(); + String formatString = ""; final List args = new ArrayList<>(); try { - PsiConcatenationUtil.buildFormatString(concatenation, formatString, args, false); + formatString = StringUtil.escapeStringCharacters(PsiConcatenationUtil.buildFormatString(concatenation, false, args)); } catch (IncorrectOperationException e) { LOG.error(e); } - return new JavaI18nizeQuickFixDialog(project, context, literalExpression, formatString.toString(), null, true, true) { + return new JavaI18nizeQuickFixDialog(project, context, literalExpression, formatString, null, true, true) { @Override @Nullable protected String getTemplateName() {