mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-24099 Fixed: False positive: parameter unfilled for NamedTuple with default value
Update PyCallableParameterImpl to be able to store default value and use this in PyNamedTupleType. Class inherited from typing.NamedTuple passes default values to PyNamedTupleType.
This commit is contained in:
committed by
Semyon Proshev
parent
27ad3a3c0a
commit
2447451dc4
@@ -45,7 +45,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
private final String myName;
|
||||
|
||||
@NotNull
|
||||
private final Map<String, Optional<PyType>> myFields;
|
||||
private final Map<String, FieldTypeAndDefaultValue> myFields;
|
||||
|
||||
@NotNull
|
||||
private final DefinitionLevel myDefinitionLevel;
|
||||
@@ -53,7 +53,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
public PyNamedTupleType(@NotNull PyClass tupleClass,
|
||||
@NotNull PsiElement declaration,
|
||||
@NotNull String name,
|
||||
@NotNull Map<String, Optional<PyType>> fields,
|
||||
@NotNull Map<String, FieldTypeAndDefaultValue> fields,
|
||||
@NotNull DefinitionLevel definitionLevel) {
|
||||
super(tupleClass, definitionLevel != DefinitionLevel.INSTANCE);
|
||||
myDeclaration = declaration;
|
||||
@@ -149,7 +149,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Optional<PyType>> getFields() {
|
||||
public Map<String, FieldTypeAndDefaultValue> getFields() {
|
||||
return myFields;
|
||||
}
|
||||
|
||||
@@ -162,14 +162,43 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType
|
||||
@Override
|
||||
public List<PyCallableParameter> getParameters(@NotNull TypeEvalContext context) {
|
||||
return isCallable()
|
||||
? ContainerUtil.map(myFields.entrySet(), field -> new PyCallableParameterImpl(field.getKey(), field.getValue().orElse(null)))
|
||||
? ContainerUtil.map(myFields.entrySet(), field -> fieldToCallableParameter(field.getKey(), field.getValue()))
|
||||
: null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static PyCallableParameter fieldToCallableParameter(@NotNull String name, @NotNull FieldTypeAndDefaultValue typeAndDefaultValue) {
|
||||
return new PyCallableParameterImpl(name, typeAndDefaultValue.getType(), typeAndDefaultValue.getDefaultValue());
|
||||
}
|
||||
|
||||
public enum DefinitionLevel {
|
||||
|
||||
AS_SUPERCLASS,
|
||||
NEW_TYPE,
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static class FieldTypeAndDefaultValue {
|
||||
|
||||
@Nullable
|
||||
private final PyType myType;
|
||||
|
||||
@Nullable
|
||||
private final PyExpression myDefaultValue;
|
||||
|
||||
public FieldTypeAndDefaultValue(@Nullable PyType type, @Nullable PyExpression defaultValue) {
|
||||
myType = type;
|
||||
myDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PyType getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PyExpression getDefaultValue() {
|
||||
return myDefaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase {
|
||||
return new PyNamedTupleType(tupleClass,
|
||||
referenceTarget,
|
||||
stub.getName(),
|
||||
parseNamedTupleFieldsTypes(referenceTarget, stub.getFields(), context),
|
||||
parseNamedTupleFields(referenceTarget, stub.getFields(), context),
|
||||
definitionLevel);
|
||||
}
|
||||
|
||||
@@ -431,27 +431,33 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LinkedHashMap<String, Optional<PyType>> parseNamedTupleFieldsTypes(@NotNull PsiElement anchor,
|
||||
@NotNull Map<String, Optional<String>> fields,
|
||||
@NotNull TypeEvalContext context) {
|
||||
final LinkedHashMap<String, Optional<PyType>> result = new LinkedHashMap<>();
|
||||
private static LinkedHashMap<String, PyNamedTupleType.FieldTypeAndDefaultValue> parseNamedTupleFields(@NotNull PsiElement anchor,
|
||||
@NotNull Map<String, Optional<String>> fields,
|
||||
@NotNull TypeEvalContext context) {
|
||||
final LinkedHashMap<String, PyNamedTupleType.FieldTypeAndDefaultValue> result = new LinkedHashMap<>();
|
||||
|
||||
for (Map.Entry<String, Optional<String>> entry : fields.entrySet()) {
|
||||
result.put(entry.getKey(), entry.getValue().map(type -> parseNamedTupleFieldType(anchor, type, context)));
|
||||
result.put(entry.getKey(), parseNamedTupleField(anchor, entry.getValue().orElse(null), context));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PyType parseNamedTupleFieldType(@NotNull PsiElement anchor, @NotNull String type, @NotNull TypeEvalContext context) {
|
||||
private static PyNamedTupleType.FieldTypeAndDefaultValue parseNamedTupleField(@NotNull PsiElement anchor,
|
||||
@Nullable String type,
|
||||
@NotNull TypeEvalContext context) {
|
||||
if (type == null) return new PyNamedTupleType.FieldTypeAndDefaultValue(null, null);
|
||||
|
||||
final PyExpressionCodeFragmentImpl codeFragment = new PyExpressionCodeFragmentImpl(anchor.getProject(), "dummy.py", type, false);
|
||||
codeFragment.setContext(anchor.getContainingFile());
|
||||
|
||||
final PsiElement element = codeFragment.getFirstChild();
|
||||
if (element instanceof PyExpressionStatement) {
|
||||
final PyExpression expression = ((PyExpressionStatement)element).getExpression();
|
||||
return Ref.deref(PyTypingTypeProvider.getType(expression, context));
|
||||
final PyType pyType = Ref.deref(PyTypingTypeProvider.getType(expression, context));
|
||||
|
||||
return new PyNamedTupleType.FieldTypeAndDefaultValue(pyType, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -172,16 +172,16 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
|
||||
}
|
||||
);
|
||||
|
||||
final Collector<PyTargetExpression, ?, LinkedHashMap<String, Optional<PyType>>> toTypedFields =
|
||||
final Collector<PyTargetExpression, ?, LinkedHashMap<String, PyNamedTupleType.FieldTypeAndDefaultValue>> toNTFields =
|
||||
Collectors.toMap(PyTargetExpression::getName,
|
||||
field -> Optional.ofNullable(context.getType(field)),
|
||||
field -> new PyNamedTupleType.FieldTypeAndDefaultValue(context.getType(field), field.findAssignedValue()),
|
||||
(v1, v2) -> v2,
|
||||
LinkedHashMap::new);
|
||||
|
||||
return new PyNamedTupleType(tupleClass,
|
||||
this,
|
||||
name,
|
||||
fields.stream().collect(toTypedFields),
|
||||
fields.stream().collect(toNTFields),
|
||||
PyNamedTupleType.DefinitionLevel.NEW_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,17 +32,24 @@ import java.util.Objects;
|
||||
public class PyCallableParameterImpl implements PyCallableParameter {
|
||||
@Nullable private final String myName;
|
||||
@Nullable private final PyType myType;
|
||||
@Nullable private final PyExpression myDefaultValue;
|
||||
@Nullable private final PyParameter myElement;
|
||||
|
||||
public PyCallableParameterImpl(@Nullable String name, @Nullable PyType type) {
|
||||
this(name, type, null);
|
||||
}
|
||||
|
||||
public PyCallableParameterImpl(@Nullable String name, @Nullable PyType type, @Nullable PyExpression defaultValue) {
|
||||
myName = name;
|
||||
myType = type;
|
||||
myDefaultValue = defaultValue;
|
||||
myElement = null;
|
||||
}
|
||||
|
||||
public PyCallableParameterImpl(@NotNull PyParameter element) {
|
||||
myName = null;
|
||||
myType = null;
|
||||
myDefaultValue = null;
|
||||
myElement = element;
|
||||
}
|
||||
|
||||
@@ -79,12 +86,12 @@ public class PyCallableParameterImpl implements PyCallableParameter {
|
||||
@Nullable
|
||||
@Override
|
||||
public PyExpression getDefaultValue() {
|
||||
return myElement == null ? null : myElement.getDefaultValue();
|
||||
return myElement == null ? myDefaultValue : myElement.getDefaultValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDefaultValue() {
|
||||
return myElement != null && myElement.hasDefaultValue();
|
||||
return myElement == null ? myDefaultValue != null : myElement.hasDefaultValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import typing
|
||||
|
||||
|
||||
class MyTup5(typing.NamedTuple):
|
||||
bar: int
|
||||
baz: str = ""
|
||||
|
||||
|
||||
# empty
|
||||
MyTup5(<warning descr="Parameter 'bar' unfilled">)</warning>
|
||||
|
||||
# one
|
||||
MyTup5('')
|
||||
MyTup5(bar='')
|
||||
MyTup5(baz=''<warning descr="Parameter 'bar' unfilled">)</warning>
|
||||
|
||||
# two
|
||||
MyTup5('', '')
|
||||
MyTup5(bar='', baz='')
|
||||
MyTup5(baz='', bar='')
|
||||
|
||||
# three
|
||||
MyTup5(bar='', baz='', <warning descr="Unexpected argument">foo=''</warning>)
|
||||
MyTup5('', '', <warning descr="Unexpected argument">''</warning>)
|
||||
@@ -26,9 +26,15 @@ class MyTup6(typing.NamedTuple):
|
||||
MyTup7 = typing.NamedTuple("MyTup7", names=List[str], ages=List[int])
|
||||
|
||||
|
||||
class MyTup8(typing.NamedTuple):
|
||||
bar: int
|
||||
baz: str = ""
|
||||
|
||||
|
||||
MyTup2(<arg1>)
|
||||
MyTup3(<arg2>)
|
||||
MyTup4(<arg3>)
|
||||
MyTup5(<arg4>)
|
||||
MyTup6(<arg5>)
|
||||
MyTup7(<arg6>)
|
||||
MyTup8(<arg7>)
|
||||
|
||||
@@ -672,7 +672,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
|
||||
runWithLanguageLevel(
|
||||
LanguageLevel.PYTHON35,
|
||||
() -> {
|
||||
final Map<String, PsiElement> test = loadTest(6);
|
||||
final Map<String, PsiElement> test = loadTest(7);
|
||||
|
||||
for (int offset : StreamEx.of(1, 2, 3, 4).map(number -> test.get("<arg" + number + ">").getTextOffset())) {
|
||||
final List<String> texts = Collections.singletonList("bar: int, baz: str");
|
||||
@@ -688,6 +688,10 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
|
||||
final List<String> texts2 = Collections.singletonList("names: List[str], ages: List[int]");
|
||||
final List<String[]> highlighted2 = Collections.singletonList(new String[]{"names: List[str], "});
|
||||
feignCtrlP(test.get("<arg6>").getTextOffset()).check(texts2, highlighted2, Collections.singletonList(ArrayUtil.EMPTY_STRING_ARRAY));
|
||||
|
||||
final List<String> texts3 = Collections.singletonList("bar: int, baz: str=\"\"");
|
||||
final List<String[]> highlighted3 = Collections.singletonList(new String[]{"bar: int, "});
|
||||
feignCtrlP(test.get("<arg7>").getTextOffset()).check(texts3, highlighted3, Collections.singletonList(ArrayUtil.EMPTY_STRING_ARRAY));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -648,12 +648,18 @@ public class PyStubsTest extends PyTestCase {
|
||||
final Iterator<String> fieldsNamesIterator = expectedFieldsNames.iterator();
|
||||
final Iterator<String> fieldsTypesIterator = expectedFieldsTypes.iterator();
|
||||
|
||||
for (Map.Entry<String, Optional<PyType>> entry : namedTupleType.getFields().entrySet()) {
|
||||
for (Map.Entry<String, PyNamedTupleType.FieldTypeAndDefaultValue> entry : namedTupleType.getFields().entrySet()) {
|
||||
assertTrue(fieldsNamesIterator.hasNext());
|
||||
assertTrue(fieldsTypesIterator.hasNext());
|
||||
|
||||
assertEquals(fieldsNamesIterator.next(), entry.getKey());
|
||||
assertEquals(fieldsTypesIterator.next(), entry.getValue().map(PyType::getName).orElse(null));
|
||||
final String fieldName = entry.getKey();
|
||||
final PyNamedTupleType.FieldTypeAndDefaultValue fieldTypeAndDefaultValue = entry.getValue();
|
||||
|
||||
assertEquals(fieldsNamesIterator.next(), fieldName);
|
||||
|
||||
final PyType fieldType = fieldTypeAndDefaultValue.getType();
|
||||
assertEquals(fieldsTypesIterator.next(), fieldType == null ? null : fieldType.getName());
|
||||
assertNull(fieldTypeAndDefaultValue.getDefaultValue());
|
||||
}
|
||||
|
||||
assertFalse(fieldsNamesIterator.hasNext());
|
||||
|
||||
@@ -278,6 +278,11 @@ public class PyArgumentListInspectionTest extends PyTestCase {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
|
||||
}
|
||||
|
||||
// PY-24099
|
||||
public void testInitializingTypingNamedTupleWithDefaultValues() {
|
||||
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
|
||||
}
|
||||
|
||||
// PY-4344, PY-8422, PY-22269, PY-22740
|
||||
public void testInitializingCollectionsNamedTuple() {
|
||||
doTest();
|
||||
|
||||
Reference in New Issue
Block a user