blob: 0ba35edad8bf496b37d51c0620fc31362c7c3b01 [file] [log] [blame]
/*
* Copyright 2000-2014 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.
*/
package com.jetbrains.python.codeInsight.intentions;
import com.intellij.codeInsight.intention.impl.BaseIntentionAction;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
import com.jetbrains.python.psi.types.PyClassType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.PyTypeChecker;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.jetbrains.python.psi.PyUtil.sure;
/**
* Replaces expressions like <code>"%s" % values</code> with likes of <code>"{0:s}".format(values)</code>.
* <br/>
* Author: Alexey.Ivanov, dcheryasov
*/
public class ConvertFormatOperatorToMethodIntention extends BaseIntentionAction {
private static final Pattern FORMAT_PATTERN =
Pattern.compile("%(?:\\((\\w+)\\))?([-#0+ ]*)((?:\\*|\\d+)?(?:\\.(?:\\*|\\d+))?)?[hlL]?([diouxXeEfFgGcrs%])");
// groups: %:ignored, 1:key 2:mods 3:width-and---preci.sion x:len 4: conversion-type
private static final Pattern BRACE_PATTERN = Pattern.compile("(\\{|\\})");
/**
* copy source to target, doubling every brace.
*/
private static void appendDoublingBraces(CharSequence source, StringBuilder target) {
int index = 0;
Matcher scanner = BRACE_PATTERN.matcher(source);
boolean skipClosingBrace = false;
while (scanner.find(index)) {
if (scanner.start() > 1) {
// handle escaping sequences PY-977
if ("{".equals(scanner.group(0)) && "\\N".equals(source.subSequence(scanner.start()-2, scanner.start()).toString())) {
skipClosingBrace = true;
target.append(source.subSequence(index, scanner.end()));
index = scanner.end();
continue;
}
}
if (skipClosingBrace && "}".equals(scanner.group(0))) {
skipClosingBrace = false;
target.append(source.subSequence(index, scanner.end()));
index = scanner.end();
continue;
}
target.append(source.subSequence(index, scanner.start()));
if ("{".equals(scanner.group(0))) target.append("{{");
else target.append("}}");
index = scanner.end();
}
target.append(source.subSequence(index, source.length()));
}
/**
* Converts format expressions inside a string
* @return a pair of string builder with resulting string expression and a flag which is true if formats inside use mapping by name.
*/
private static Pair<StringBuilder, Boolean> convertFormat(PyStringLiteralExpression stringLiteralExpression, String prefix) {
// python string may be made of several literals, all different
List<StringBuilder> constants = new ArrayList<StringBuilder>();
boolean usesNamedFormat = false;
final List<ASTNode> stringNodes = stringLiteralExpression.getStringNodes();
sure(stringNodes);
sure(stringNodes.size() > 0);
for (ASTNode stringNode : stringNodes) {
// preserve prefixes and quote form
CharSequence text = stringNode.getChars();
int openPos = 0;
boolean hasPrefix = false;
final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(String.valueOf(text));
if (prefixLength != 0) hasPrefix = true;
openPos += prefixLength;
char quote = text.charAt(openPos);
sure("\"'".indexOf(quote) >= 0);
if (text.length() - openPos >= 6) {
// triple-quoted?
if (text.charAt(openPos+1) == quote && text.charAt(openPos+2) == quote) {
openPos += 2;
}
}
int index = openPos + 1; // from quote to first in-string char
StringBuilder out = new StringBuilder(text.subSequence(0, openPos+1));
if (!hasPrefix) out.insert(0, prefix);
int position_count = 0;
Matcher scanner = FORMAT_PATTERN.matcher(text);
while (scanner.find(index)) {
// store previous non-format part
appendDoublingBraces(text.subSequence(index, scanner.start()), out);
//out.append(text.subSequence(index, scanner.start()));
// unpack format
final String f_key = scanner.group(1);
final String f_modifier = scanner.group(2);
final String f_width = scanner.group(3);
String f_conversion = scanner.group(4);
// convert to format()'s
if ("%%".equals(scanner.group(0))) {
// shortcut to put a literal %
out.append("%");
}
else {
sure(f_conversion);
sure(!"%".equals(f_conversion)); // a padded percent literal; can't bother to autoconvert, and in 3k % is different.
out.append("{");
if (f_key != null) {
out.append(f_key);
usesNamedFormat = true;
}
else {
out.append(position_count);
position_count += 1;
}
if ("r".equals(f_conversion)) out.append("!r");
// don't convert %s -> !s, for %s is the normal way to output the default representation
out.append(":");
if (f_modifier != null) {
// in strict order
if (has(f_modifier, '-')) out.append("<"); // left align
else if ("s".equals(f_conversion) && !StringUtil.isEmptyOrSpaces(f_width)) {
// "%20s" aligns right, "{0:20s}" aligns left; to preserve align, make it explicit
out.append(">");
}
if (has(f_modifier, '+')) out.append("+"); // signed
else if (has(f_modifier, ' ')) out.append(" "); // default-signed
if (has(f_modifier, '#')) out.append("#"); // alt numbers
if (has(f_modifier, '0')) out.append("0"); // padding
// anything else can't be here
}
if (f_width != null) {
out.append(f_width);
}
if ("i".equals(f_conversion) || "u".equals(f_conversion)) out.append("d");
else if ("r".equals(f_conversion)) out.append("s"); // we want our raw string as a string
else out.append(f_conversion);
//
out.append("}");
}
index = scanner.end();
}
// store non-format final part
//out.append(text.subSequence(index, text.length()-1));
appendDoublingBraces(text.subSequence(index, text.length()), out);
constants.add(out);
}
// form the entire literal filling possible gaps between constants.
// we assume that a string literal begins with its first constant, without a gap.
TextRange full_range = stringLiteralExpression.getTextRange();
int full_start = full_range.getStartOffset();
CharSequence full_text = stringLiteralExpression.getNode().getChars();
TextRange prev_range = stringNodes.get(0).getTextRange();
int fragment_no = 1; // look at second and further fragments
while (fragment_no < stringNodes.size()) {
TextRange next_range = stringNodes.get(fragment_no).getTextRange();
int left = prev_range.getEndOffset() - full_start;
int right = next_range.getStartOffset() - full_start;
if (left < right) {
constants.get(fragment_no-1).append(full_text.subSequence(left, right));
}
fragment_no += 1;
prev_range = next_range;
}
final int left = prev_range.getEndOffset() - full_start;
final int right = full_range.getEndOffset() - full_start;
if (left < right) {
// the barely possible case of last dangling "\"
constants.get(constants.size()-1).append(full_text.subSequence(left, right));
}
// join everything
StringBuilder result = new StringBuilder();
for (StringBuilder one : constants) result.append(one);
return new Pair<StringBuilder, Boolean>(result, usesNamedFormat);
}
private static boolean has(String where, char what) {
return where.indexOf(what) >= 0;
}
@NotNull
public String getFamilyName() {
return PyBundle.message("INTN.format.operator.to.method");
}
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
if (!(file instanceof PyFile)) {
return false;
}
PyBinaryExpression binaryExpression =
PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PyBinaryExpression.class, false);
if (binaryExpression == null) {
return false;
}
final LanguageLevel languageLevel = LanguageLevel.forElement(binaryExpression);
if (languageLevel.isOlderThan(LanguageLevel.PYTHON26)) {
return false;
}
if (binaryExpression.getLeftExpression() instanceof PyStringLiteralExpression && binaryExpression.getOperator() == PyTokenTypes.PERC) {
setText(PyBundle.message("INTN.replace.with.method"));
return true;
}
return false;
}
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
final PsiElement elementAt = file.findElementAt(editor.getCaretModel().getOffset());
final PyBinaryExpression element = PsiTreeUtil.getParentOfType(elementAt, PyBinaryExpression.class, false);
if (element == null) return;
final PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project);
final PyExpression rightExpression = sure(element).getRightExpression();
if (rightExpression == null) {
return;
}
final PyExpression rhs = PyPsiUtils.flattenParens(rightExpression);
if (rhs == null) return;
final String paramText = sure(rhs).getText();
final TypeEvalContext context = TypeEvalContext.userInitiated(file);
final PyType rhsType = context.getType(rhs);
String prefix = "";
if (PyTypeChecker.match(PyBuiltinCache.getInstance(rhs).getObjectType("unicode"), rhsType, context)) {
prefix = "u";
}
final PyStringLiteralExpression leftExpression = (PyStringLiteralExpression)element.getLeftExpression();
final Pair<StringBuilder, Boolean> converted = convertFormat(leftExpression, prefix);
final StringBuilder target = converted.getFirst();
final String separator = getSeparator(leftExpression);
target.append(separator).append(".format");
if (rhs instanceof PyDictLiteralExpression) target.append("(**").append(paramText).append(")");
else if (rhs instanceof PyCallExpression) { // potential dict(foo=1) -> format(foo=1)
final PyCallExpression callExpression = (PyCallExpression)rhs;
final PyExpression callee = callExpression.getCallee();
if (callee instanceof PyReferenceExpression) {
PsiElement maybeDict = ((PyReferenceExpression)callee).getReference().resolve();
if (maybeDict instanceof PyFunction) {
PyFunction dictInit = (PyFunction)maybeDict;
if (PyNames.INIT.equals(dictInit.getName())) {
final PyClassType dictType = PyBuiltinCache.getInstance(file).getDictType();
if (dictType != null && dictType.getPyClass() == dictInit.getContainingClass()) {
target.append(sure(sure(callExpression.getArgumentList()).getNode()).getChars());
}
}
else { // just a call, reuse
target.append("(");
if (converted.getSecond()) target.append("**"); // map-by-name formatting was detected
target.append(paramText).append(")");
}
}
}
}
else target.append("(").append(paramText).append(")"); // tuple is ok as is
// Correctly handle multiline implicitly concatenated string literals (PY-9176)
target.insert(0, '(').append(')');
final PyExpression parenthesized = elementGenerator.createExpressionFromText(LanguageLevel.forElement(element), target.toString());
element.replace(sure(((PyParenthesizedExpression)parenthesized).getContainedExpression()));
}
private static String getSeparator(PyStringLiteralExpression leftExpression) {
String separator = ""; // detect nontrivial whitespace around the "%"
Pair<String, PsiElement> crop = collectWhitespace(leftExpression);
String maybeSeparator = crop.getFirst();
if (maybeSeparator != null && !maybeSeparator.isEmpty() && !" ".equals(maybeSeparator))
separator = maybeSeparator;
else { // after "%"
crop = collectWhitespace(crop.getSecond());
maybeSeparator = crop.getFirst();
if (maybeSeparator != null && !maybeSeparator.isEmpty() && !" ".equals(maybeSeparator))
separator = maybeSeparator;
}
return separator;
}
private static Pair<String, PsiElement> collectWhitespace(PsiElement start) {
StringBuilder sb = new StringBuilder();
PsiElement seeker = start;
while (seeker != null) {
seeker = seeker.getNextSibling();
if (seeker != null && seeker instanceof PsiWhiteSpace) sb.append(seeker.getText());
else break;
}
return Pair.create(sb.toString(), seeker);
}
}