// Copyright 2000-2020 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; import com.intellij.application.options.CodeStyle; import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature; import com.intellij.java.JavaBundle; import com.intellij.lang.java.JavaLanguage; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.util.PsiLiteralUtil; import com.siyeh.ig.psiutils.CommentTracker; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import java.util.StringJoiner; import static com.intellij.util.ObjectUtils.tryCast; public class TextBlockBackwardMigrationInspection extends AbstractBaseJavaLocalInspectionTool { @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { if (!HighlightingFeature.TEXT_BLOCKS.isAvailable(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR; return new JavaElementVisitor() { @Override public void visitLiteralExpression(PsiLiteralExpression expression) { if (!expression.isTextBlock()|| PsiLiteralUtil.getTextBlockText(expression) == null) { return; } holder.registerProblem(expression, JavaBundle.message("inspection.text.block.backward.migration.message"), new ReplaceWithRegularStringLiteralFix()); } }; } private static class ReplaceWithRegularStringLiteralFix implements LocalQuickFix { @Nls(capitalization = Nls.Capitalization.Sentence) @NotNull @Override public String getFamilyName() { return JavaBundle.message("inspection.replace.with.regular.string.literal.fix"); } @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { PsiLiteralExpression literalExpression = tryCast(descriptor.getPsiElement(), PsiLiteralExpression.class); if (literalExpression == null || !literalExpression.isTextBlock()) return; String text = PsiLiteralUtil.getTextBlockText(literalExpression); if (text == null) return; String replacement = convertToConcatenation(text); PsiFile file = descriptor.getPsiElement().getContainingFile(); if (file == null) return; CodeStyleSettings tempSettings = CodeStyle.getSettings(file); tempSettings.getCommonSettings(JavaLanguage.INSTANCE).ALIGN_MULTILINE_BINARY_OPERATION = true; CodeStyleManager manager = CodeStyleManager.getInstance(literalExpression.getProject()); CodeStyle.doWithTemporarySettings(project, tempSettings, () -> { PsiElement result = new CommentTracker().replaceAndRestoreComments(literalExpression, replacement); manager.reformat(result); }); } @NotNull private static String convertToConcatenation(@NotNull String text) { if (text.isEmpty()) return "\"\""; StringJoiner joiner = new StringJoiner(" +\n"); String[] lines = getTextBlockLines(text).split("\n", -1); for (int i = 0; i < lines.length; i++) { String line = lines[i]; boolean addNewLine = i != lines.length - 1; if (!addNewLine && line.isEmpty()) break; joiner.add("\"" + line + (addNewLine ? "\\n\"" : "\"")); } return joiner.toString(); } @NotNull private static String getTextBlockLines(@NotNull String text) { int length = text.length(); StringBuilder result = new StringBuilder(length); int i = 0; while (i < length) { int nSlashes = 0; int next = i; while (next < length && (next = PsiLiteralUtil.parseBackSlash(text, next)) != -1) { nSlashes++; i = next; } if (i >= length) { result.append(StringUtil.repeatSymbol('\\', nSlashes)); break; } next = parseQuote(i, text, nSlashes, result); if (next != -1) { i = next; continue; } if (nSlashes != 0) { i = parseEscapedChar(i, text, nSlashes, result); } else { result.append(text.charAt(i)); i++; } } return result.toString(); } private static int parseEscapedChar(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) { int next = parseEscapedSpace(i, text, nSlashes, result); if (next != -1) return next; next = parseEscapedLineBreak(i, text, nSlashes, result); if (next != -1) return next; result.append(StringUtil.repeatSymbol('\\', nSlashes)).append(text.charAt(i)); return i + 1; } private static int parseEscapedSpace(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) { char c = text.charAt(i); if (c == 's' && nSlashes % 2 != 0) { result.append(StringUtil.repeatSymbol('\\', nSlashes - 1)).append(' '); return i + 1; } if (StringUtil.startsWith(text, i, "040") && nSlashes % 2 != 0) { result.append(StringUtil.repeatSymbol('\\', nSlashes - 1)).append(' '); return i + 3; } return -1; } private static int parseEscapedLineBreak(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) { char c = text.charAt(i); if (c == '\n' && nSlashes % 2 != 0) { result.append(StringUtil.repeatSymbol('\\', nSlashes - 1)); return i + 1; } return -1; } private static int parseQuote(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) { char c = text.charAt(i); if (c != '"') return -1; if (nSlashes % 2 == 0) nSlashes++; result.append(StringUtil.repeatSymbol('\\', nSlashes)).append(c); return i + 1; } } }