PY-8325 Analyze call expression return type to determine number of arguments Add tests

to squash
This commit is contained in:
Valentina Kiryushkina
2017-05-15 14:37:53 +03:00
parent 26db023a9c
commit bcc9d013a6
5 changed files with 265 additions and 58 deletions
@@ -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);
}
@@ -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
<warning descr="Too few mapping keys">"{}{}"</warning>.format(f("i"))
<warning descr="Too few mapping keys">"{}{}"</warning>.format(f("f"))
<warning descr="Too few mapping keys">"{}{}"</warning>.format(f("s"))
<warning descr="Too few mapping keys">"{}{}"</warning>.format(f("b"))
@@ -0,0 +1,28 @@
def bar():
return 1
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return 1.0
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return ""
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return True
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return []
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return {}
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
def bar():
return set()
"%s %s" % <warning descr="Too few arguments for format string">bar()</warning>
@@ -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" % <warning descr="Too few arguments for format string">f("i")</warning>
def bar():
return 1.0
"%s %s" % <warning descr="Too few arguments for format string">f("f")</warning>
def bar():
return ""
"%s %s" % <warning descr="Too few arguments for format string">f("s")</warning>
def bar():
return True
"%s %s" % <warning descr="Too few arguments for format string">f("b")</warning>
def bar():
return []
"%s %s" % <warning descr="Too few arguments for format string">f("l")</warning>
def bar():
return {}
"%s %s" % <warning descr="Too few arguments for format string">f("d")</warning>
def bar():
return set()
"%s %s" % <warning descr="Too few arguments for format string">f("set")</warning>
@@ -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);