From bcc9d013a67b9cd5eee5c49e3d6d795492181959 Mon Sep 17 00:00:00 2001 From: Valentina Kiryushkina Date: Mon, 27 Jun 2016 15:37:00 +0300 Subject: [PATCH] PY-8325 Analyze call expression return type to determine number of arguments Add tests to squash --- .../inspections/PyStringFormatInspection.java | 148 +++++++++++------- .../NewStyleCallExpressionArgument.py | 14 ++ .../PercentStringCallArgument.py | 28 ++++ .../PercentStringCallUnionArgument.py | 41 +++++ .../PyStringFormatInspectionTest.java | 92 +++++++++++ 5 files changed, 265 insertions(+), 58 deletions(-) create mode 100644 python/testData/inspections/PyStringFormatInspection/NewStyleCallExpressionArgument.py create mode 100644 python/testData/inspections/PyStringFormatInspection/PercentStringCallArgument.py create mode 100644 python/testData/inspections/PyStringFormatInspection/PercentStringCallUnionArgument.py 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);