diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java b/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java
index f02ed70a8660..5e13ae5c6f45 100644
--- a/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java
+++ b/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java
@@ -40,6 +40,7 @@ import org.jetbrains.annotations.Nullable;
import java.math.BigInteger;
import java.util.*;
+import java.util.stream.Collectors;
import static com.jetbrains.python.inspections.PyStringFormatParser.filterSubstitutions;
import static com.jetbrains.python.inspections.PyStringFormatParser.parsePercentFormat;
@@ -161,30 +162,9 @@ public class PyStringFormatInspection extends PyInspection {
return inspectArguments((PyExpression)pyElement, problemTarget);
}
else if (rightExpression instanceof PyCallExpression) {
- final PyCallExpression call = (PyCallExpression)rightExpression;
-
- final IntSummaryStatistics statistics = call.multiResolveCalleeFunction(resolveContext)
- .stream()
- .map(callable -> callable.getCallType(myTypeEvalContext, call))
- .collect(
- Collectors.summarizingInt(
- callType -> {
- if (callType instanceof PyTupleType) {
- return ((PyTupleType)callType).getElementCount();
- }
- else {
- return 1;
- }
- }
- )
- );
-
- if (statistics.getMin() == statistics.getMax()) {
- return statistics.getMin();
- }
- else {
- return -1;
- }
+ final PyExpression callee = ((PyCallExpression)rightExpression).getCallee();
+ if (callee != null && "dict".equals(callee.getName())) return 1;
+ return inspectCallExpression((PyCallExpression)rightExpression, resolveContext, myTypeEvalContext, true);
}
else if (rightExpression instanceof PyParenthesizedExpression) {
final PyExpression rhs = ((PyParenthesizedExpression)rightExpression).getContainedExpression();
@@ -590,36 +570,6 @@ public class PyStringFormatInspection extends PyInspection {
}
}
- private int inspectCallExpression(@NotNull PyCallExpression callExpression, @NotNull PyResolveContext resolveContext) {
- final PyReturnStatement[] returnStatements = getFunctionReturnValues(callExpression, resolveContext);
- int expressionsSize = -1;
- for (PyReturnStatement returnStatement : returnStatements) {
- if (returnStatement.getExpression() instanceof PyCallExpression) {
- return -1;
- }
- final int argumentsSize = Math.max(PyUtil.flattenedParensAndTuples(returnStatement.getExpression()).size(),
- PyUtil.flattenedParensAndLists(returnStatement.getExpression()).size());
- if (expressionsSize < 0) {
- expressionsSize = argumentsSize;
- }
- if (expressionsSize != argumentsSize) {
- return -1;
- }
- }
- return expressionsSize;
- }
-
-
- private PyReturnStatement[] getFunctionReturnValues(@NotNull PyCallExpression callExpression,
- @NotNull PyResolveContext resolveContext) {
- final PyCallable callable = callExpression.resolveCalleeFunction(resolveContext);
- if (callable instanceof PyFunction && myTypeEvalContext.maySwitchToAST(callable)) {
- PyStatementList statementList = ((PyFunction)callable).getStatementList();
- return PyUtil.getAllChildrenOfType(statementList, PyReturnStatement.class);
- }
- return new PyReturnStatement[0];
- }
-
private void registerProblem(@NotNull PsiElement problemTarget, @NotNull final String message) {
myProblemRegister = true;
myVisitor.registerProblem(problemTarget, message);
@@ -660,7 +610,8 @@ public class PyStringFormatInspection extends PyInspection {
indexElement);
}
else if (inspectedElement instanceof PyCallExpression) {
- final int callResultsArgumentsNumber = inspectCallExpression((PyCallExpression)inspectedElement, resolveContext);
+ final int callResultsArgumentsNumber = inspectCallExpression((PyCallExpression)inspectedElement, resolveContext,
+ myTypeEvalContext, false);
if (callResultsArgumentsNumber <= index) {
registerProblem(inspectedElement, PyBundle.message("INSP.too.few.args.for.fmt.string"));
}
@@ -671,10 +622,10 @@ public class PyStringFormatInspection extends PyInspection {
}
catch (NumberFormatException e) {
if (inspectedElement instanceof PyCallExpression) {
- final PyReturnStatement[] returnValues = getFunctionReturnValues((PyCallExpression)inspectedElement, resolveContext);
+ final PyReturnStatement[] returnValues = getFunctionReturnValues((PyCallExpression)inspectedElement, resolveContext,
+ myTypeEvalContext);
for (PyReturnStatement value : returnValues) {
- PyExpression valueExpression = value.getExpression();
- valueExpression = PyPsiUtils.flattenParens(valueExpression);
+ PyExpression valueExpression = PyPsiUtils.flattenParens(value.getExpression());
if (valueExpression instanceof PyDictLiteralExpression) {
inspectDictForKey(formatExpression, inspectedElement, (PyDictLiteralExpression)valueExpression, mappingKey,
indexElement);
@@ -818,6 +769,87 @@ public class PyStringFormatInspection extends PyInspection {
}
}
+ static int inspectCallExpression(@NotNull PyCallExpression callExpression,
+ @NotNull PyResolveContext resolveContext,
+ @NotNull TypeEvalContext evalContext,
+ boolean isPercent) {
+ final IntSummaryStatistics statistics = callExpression.multiResolveCalleeFunction(resolveContext)
+ .stream()
+ .map(callable -> callable.getCallType(evalContext, callExpression))
+ .collect(
+ Collectors.summarizingInt(
+ callType -> {
+ if (callType instanceof PyTupleType) {
+ return ((PyTupleType)callType).getElementCount();
+ }
+ else if (callType instanceof PyCollectionTypeImpl
+ && ((PyCollectionTypeImpl)callType).getElementTypes(evalContext).size() == 1) {
+ if (isPercent) return 1;
+
+ final PyClass pyClass = ((PyCollectionTypeImpl)callType).getPyClass();
+ if ("list".equals(pyClass.getName())) {
+ final PyReturnStatement[] returnStatements = getFunctionReturnValues(callExpression, resolveContext, evalContext);
+ int expressionsSize = -1;
+ for (PyReturnStatement returnStatement : returnStatements) {
+ if (returnStatement.getExpression() instanceof PyCallExpression) {
+ return -1;
+ }
+ final int argumentsSize = PyUtil.flattenedParensAndLists(returnStatement.getExpression()).size();
+ if (expressionsSize < 0) {
+ expressionsSize = argumentsSize;
+ }
+ if (expressionsSize != argumentsSize) {
+ return -1;
+ }
+ }
+ return expressionsSize;
+ }
+ }
+ else if (callType instanceof PyNoneType) {
+ return 1;
+ }
+ else if (callType instanceof PyClassType) {
+ final PyClassType setType = PyBuiltinCache.getInstance(callExpression).getSetType();
+ final PyClassType tupleType = PyBuiltinCache.getInstance(callExpression).getTupleType();
+
+ if (!callType.equals(tupleType) &&
+ (callType.equals(setType) && isPercent
+ || PyBuiltinCache.getInstance(callExpression).isBuiltin(((PyClassType)callType).getPyClass()))) {
+ return 1;
+ }
+ }
+ else if (callType instanceof PyUnionType) {
+ if (((PyUnionType)callType).getMembers().stream().allMatch(PyType::isBuiltin)) return 1;
+ }
+
+ else {
+ return 1;
+ }
+ }
+ )
+ );
+
+ if (statistics.getMin() == statistics.getMax()) {
+ return statistics.getMin();
+ }
+ else {
+ return -1;
+ }
+
+ return -1;
+ }
+
+ private static PyReturnStatement[] getFunctionReturnValues(@NotNull PyCallExpression callExpression,
+ @NotNull PyResolveContext resolveContext,
+ @NotNull TypeEvalContext evalContext) {
+ final PyCallable callable = callExpression.resolveCalleeFunction(resolveContext);
+ if (callable instanceof PyFunction && evalContext.maySwitchToAST(callable)) {
+ PyStatementList statementList = ((PyFunction)callable).getStatementList();
+ return PyUtil.getAllChildrenOfType(statementList, PyReturnStatement.class);
+ }
+ return new PyReturnStatement[0];
+ }
+
public Visitor(final ProblemsHolder holder, LocalInspectionToolSession session) {
super(holder, session);
}
diff --git a/python/testData/inspections/PyStringFormatInspection/NewStyleCallExpressionArgument.py b/python/testData/inspections/PyStringFormatInspection/NewStyleCallExpressionArgument.py
new file mode 100644
index 000000000000..888b8fbc0470
--- /dev/null
+++ b/python/testData/inspections/PyStringFormatInspection/NewStyleCallExpressionArgument.py
@@ -0,0 +1,14 @@
+def f(mode):
+ if mode == "i":
+ return 1
+ elif mode == "f":
+ return 1.0
+ elif mode == "s":
+ return ""
+ elif mode == "b":
+ return True
+
+"{}{}".format(f("i"))
+"{}{}".format(f("f"))
+"{}{}".format(f("s"))
+"{}{}".format(f("b"))
\ No newline at end of file
diff --git a/python/testData/inspections/PyStringFormatInspection/PercentStringCallArgument.py b/python/testData/inspections/PyStringFormatInspection/PercentStringCallArgument.py
new file mode 100644
index 000000000000..8127077ff88f
--- /dev/null
+++ b/python/testData/inspections/PyStringFormatInspection/PercentStringCallArgument.py
@@ -0,0 +1,28 @@
+def bar():
+ return 1
+"%s %s" % bar()
+
+def bar():
+ return 1.0
+"%s %s" % bar()
+
+def bar():
+ return ""
+"%s %s" % bar()
+
+def bar():
+ return True
+"%s %s" % bar()
+
+def bar():
+ return []
+"%s %s" % bar()
+
+def bar():
+ return {}
+"%s %s" % bar()
+
+def bar():
+ return set()
+"%s %s" % bar()
+
diff --git a/python/testData/inspections/PyStringFormatInspection/PercentStringCallUnionArgument.py b/python/testData/inspections/PyStringFormatInspection/PercentStringCallUnionArgument.py
new file mode 100644
index 000000000000..c768b109ff27
--- /dev/null
+++ b/python/testData/inspections/PyStringFormatInspection/PercentStringCallUnionArgument.py
@@ -0,0 +1,41 @@
+def f(mode):
+ if mode == "i":
+ return 1
+ elif mode == "f":
+ return 1.0
+ elif mode == "s":
+ return ""
+ elif mode == "b":
+ return True
+ elif mode == "l":
+ return []
+ elif mode == "d":
+ return {}
+ elif mode == "set":
+ return set()
+"%s %s" % f("i")
+
+def bar():
+ return 1.0
+"%s %s" % f("f")
+
+def bar():
+ return ""
+"%s %s" % f("s")
+
+def bar():
+ return True
+"%s %s" % f("b")
+
+def bar():
+ return []
+"%s %s" % f("l")
+
+def bar():
+ return {}
+"%s %s" % f("d")
+
+def bar():
+ return set()
+"%s %s" % f("set")
+
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java
index 6c7b76e1cd16..9f3406e69e6e 100644
--- a/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java
@@ -96,6 +96,98 @@ public class PyStringFormatInspectionTest extends PyTestCase {
doTest();
}
+ public void testNewStyleStringWithPercentSymbol() {
+ doTest();
+ }
+
+ public void testNewStylePackedAndNonPackedArgs() {
+ doTest();
+ }
+
+ public void testNewStyleEmptyDictArg() {
+ doTest();
+ }
+
+ public void testNewStyleDictLiteralExprInsideDictCall() {
+ doTest();
+ }
+
+ public void testNewStylePositionalSubstitutionWithDictArg() {
+ doTest();
+ }
+
+ public void testNewStylePackedReference() {
+ doTest();
+ }
+
+ public void testNewStylePackedFunctionCall() {
+ doTest();
+ }
+
+ public void testNewStyleStringRegularExpression() {
+ doTest();
+ }
+
+ public void testNewStyleStringMapArg() {
+ doTest();
+ }
+
+ public void testNewStyleDictLiteralWithReferenceKeys() {
+ doTest();
+ }
+
+ public void testNewStyleDictLiteralWithNumericKeys() {
+ doTest();
+ }
+
+ public void testNewStyleCallExpressionArgument() {
+ doTest();
+ }
+
+ public void testPercentStringWithFormatStringReplacementSymbols() {
+ doTest();
+ }
+
+ public void testPercentStringPositionalWithEmptyDictArg() {
+ doTest();
+ }
+
+ public void testPercentStringWithDictElement() {
+ doTest();
+ }
+
+ public void testPercentStringWithDictCall() {
+ doTest();
+ }
+
+ public void testPercentStringWithDictArgument() {
+ doTest();
+ }
+
+ public void testPercentStringPositionalListArgument() {
+ doTest();
+ }
+
+ public void testPercentStringPositionalDictArgument() {
+ doTest();
+ }
+
+ public void testPercentStringKeywordSetArgument() {
+ doTest();
+ }
+
+ public void testPercentStringKeywordListArgument() {
+ doTest();
+ }
+
+ public void testPercentStringCallUnionArgument() {
+ doTest();
+ }
+
+ public void testPercentStringCallArgument() {
+ doTest();
+ }
+
private void doTest() {
myFixture.configureByFile(TEST_DIRECTORY + getTestName(false) + ".py");
myFixture.enableInspections(PyStringFormatInspection.class);