From 8fc1ce69138d7e62d7adf48c6839c46b6cc1ec44 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Thu, 18 Feb 2016 18:08:09 +0300 Subject: [PATCH] PyNamedTupleStub: creating: check that namedtuple(...) is called, locally resolve tuple name and fields IO: deserialize and serialize: callee name, tuple name, fields number and fields names using: update PyStdlibTypeProvider to use stubs for namedtuples --- .../python/psi/stubs/PyNamedTupleStub.java | 30 ++ python/src/META-INF/python-core-common.xml | 2 + .../codeInsight/stdlib/PyNamedTupleType.java | 83 +---- .../stdlib/PyStdlibTypeProvider.java | 66 +++- .../python/psi/PyFileElementType.java | 4 +- .../psi/impl/stubs/PyNamedTupleStubImpl.java | 352 ++++++++++++++++++ .../psi/impl/stubs/PyNamedTupleStubType.java | 39 ++ .../stubs/FullyQualifiedNamedTuple.py | 3 + .../stubs/FullyQualifiedNamedTupleWithAs.py | 3 + python/testData/stubs/ImportedNamedTuple.py | 3 + .../stubs/ImportedNamedTupleFields.py | 4 + .../testData/stubs/ImportedNamedTupleName.py | 4 + .../stubs/ImportedNamedTupleWithAs.py | 3 + .../testData/stubs/NamedTupleFieldsChain.py | 5 + .../stubs/NamedTupleFieldsReference.py | 4 + .../stubs/NamedTupleFieldsSequence.py | 3 + python/testData/stubs/NamedTupleNameChain.py | 5 + .../testData/stubs/NamedTupleNameReference.py | 4 + .../com/jetbrains/python/PyStubsTest.java | 115 +++++- 19 files changed, 632 insertions(+), 100 deletions(-) create mode 100644 python/psi-api/src/com/jetbrains/python/psi/stubs/PyNamedTupleStub.java create mode 100644 python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubImpl.java create mode 100644 python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubType.java create mode 100644 python/testData/stubs/FullyQualifiedNamedTuple.py create mode 100644 python/testData/stubs/FullyQualifiedNamedTupleWithAs.py create mode 100644 python/testData/stubs/ImportedNamedTuple.py create mode 100644 python/testData/stubs/ImportedNamedTupleFields.py create mode 100644 python/testData/stubs/ImportedNamedTupleName.py create mode 100644 python/testData/stubs/ImportedNamedTupleWithAs.py create mode 100644 python/testData/stubs/NamedTupleFieldsChain.py create mode 100644 python/testData/stubs/NamedTupleFieldsReference.py create mode 100644 python/testData/stubs/NamedTupleFieldsSequence.py create mode 100644 python/testData/stubs/NamedTupleNameChain.py create mode 100644 python/testData/stubs/NamedTupleNameReference.py diff --git a/python/psi-api/src/com/jetbrains/python/psi/stubs/PyNamedTupleStub.java b/python/psi-api/src/com/jetbrains/python/psi/stubs/PyNamedTupleStub.java new file mode 100644 index 000000000000..b31125d6d81d --- /dev/null +++ b/python/psi-api/src/com/jetbrains/python/psi/stubs/PyNamedTupleStub.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2016 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.psi.stubs; + +import com.jetbrains.python.psi.impl.stubs.CustomTargetExpressionStub; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface PyNamedTupleStub extends CustomTargetExpressionStub { + + @NotNull + String getName(); + + @NotNull + List getFields(); +} diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 40b3e80d0b29..e3632161f23c 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -643,6 +643,8 @@ + + diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java index feaaceea053a..0fe6a5c60084 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyNamedTupleType.java @@ -16,17 +16,15 @@ package com.jetbrains.python.codeInsight.stdlib; import com.intellij.codeInsight.lookup.LookupElementBuilder; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.util.ArrayUtil; import com.intellij.util.ProcessingContext; -import com.jetbrains.python.PyNames; -import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.impl.PyBuiltinCache; +import com.jetbrains.python.psi.AccessDirection; +import com.jetbrains.python.psi.PyCallSiteExpression; +import com.jetbrains.python.psi.PyClass; +import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.impl.PyElementImpl; -import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.psi.resolve.PyResolveContext; -import com.jetbrains.python.psi.resolve.QualifiedResolveResult; import com.jetbrains.python.psi.resolve.RatedResolveResult; import com.jetbrains.python.psi.types.*; import org.jetbrains.annotations.NotNull; @@ -35,7 +33,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; /** * @author yole @@ -70,6 +67,7 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType return classMembers; } if (myFields.contains(name)) { + // It's important to make a copy of declaration otherwise members will have the same type as their class return Collections.singletonList(new RatedResolveResult(RatedResolveResult.RATE_HIGH, new PyElementImpl(myDeclaration.getNode()))); } return null; @@ -114,77 +112,6 @@ public class PyNamedTupleType extends PyClassTypeImpl implements PyCallableType return "PyNamedTupleType: " + myName; } - @NotNull - @Override - public Set getMemberNames(boolean inherited, @NotNull TypeEvalContext context) { - final Set result = super.getMemberNames(inherited, context); - result.addAll(myFields); - - return result; - } - - @Nullable - public static PyType fromCall(@NotNull PyCallExpression call, @NotNull TypeEvalContext context, int level) { - final String name = PyPsiUtils.strValue(call.getArgument(0, PyExpression.class)); - final PyExpression fieldsExpression = resolveFieldsExpression(call, context); - - if (name == null || fieldsExpression == null) { - return null; - } - - final List fieldNames = getFieldNames(fieldsExpression); - - if (fieldNames != null) { - PyClass tuple = PyBuiltinCache.getInstance(call).getClass(PyNames.FAKE_NAMEDTUPLE); - if (tuple != null) { - return new PyNamedTupleType(tuple, call, name, fieldNames, level); - } - } - - return null; - } - - @Nullable - private static PyExpression resolveFieldsExpression(@NotNull PyCallExpression call, @NotNull TypeEvalContext context) { - final PyExpression fieldsExpression = PyPsiUtils.flattenParens(call.getArgument(1, PyExpression.class)); - - if (fieldsExpression instanceof PyReferenceExpression) { - final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); - final QualifiedResolveResult resolveResult = ((PyReferenceExpression)fieldsExpression).followAssignmentsChain(resolveContext); - - final PsiElement resolvedFieldsExpression = resolveResult.getElement(); - - if (resolvedFieldsExpression instanceof PyExpression) { - return (PyExpression)resolvedFieldsExpression; - } - } - - return fieldsExpression; - } - - @Nullable - private static List getFieldNames(@NotNull PyExpression fieldsExpression) { - if (fieldsExpression instanceof PySequenceExpression) { - return PyUtil.strListValue(fieldsExpression); - } - else { - return parseFieldNames(PyPsiUtils.strValue(fieldsExpression)); - } - } - - @Nullable - private static List parseFieldNames(@Nullable String fieldsString) { - if (fieldsString == null) { - return null; - } - - final List result = new ArrayList(); - for (String name : StringUtil.tokenize(fieldsString, ", ")) { - result.add(name); - } - return result; - } - public int getElementCount() { return myFields.size(); } diff --git a/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java b/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java index 3d573aa35e31..68fa9d462c03 100644 --- a/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java +++ b/python/src/com/jetbrains/python/codeInsight/stdlib/PyStdlibTypeProvider.java @@ -26,8 +26,11 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyTypeProvider; +import com.jetbrains.python.psi.impl.stubs.PyNamedTupleStubImpl; import com.jetbrains.python.psi.resolve.PyResolveContext; import com.jetbrains.python.psi.resolve.QualifiedNameFinder; +import com.jetbrains.python.psi.stubs.PyNamedTupleStub; +import com.jetbrains.python.psi.stubs.PyTargetExpressionStub; import com.jetbrains.python.psi.types.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -241,27 +244,16 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase { @Nullable PsiElement anchor) { if (referenceTarget instanceof PyTargetExpression) { final PyTargetExpression target = (PyTargetExpression)referenceTarget; - final QualifiedName calleeName = target.getCalleeName(); - if (calleeName != null && PyNames.NAMEDTUPLE.equals(calleeName.toString())) { - // TODO: Create stubs for namedtuple for preventing switch from stub to AST - final PyExpression value = target.findAssignedValue(); - if (value instanceof PyCallExpression) { - final PyCallExpression call = (PyCallExpression)value; - final PyCallExpression.PyMarkedCallee callee = call.resolveCallee(PyResolveContext.noImplicits()); - if (callee != null) { - final PyCallable callable = callee.getCallable(); - if (PyNames.COLLECTIONS_NAMEDTUPLE.equals(callable.getQualifiedName())) { - return PyNamedTupleType.fromCall(call, context, 1); - } - } - } + final PyTargetExpressionStub stub = target.getStub(); + + if (stub != null) { + return getNamedTupleTypeFromStub(target, stub.getCustomStub(PyNamedTupleStub.class), 1); + } else { + return getNamedTupleTypeFromAST(target, context, 1); } } else if (referenceTarget instanceof PyFunction && anchor instanceof PyCallExpression) { - final PyFunction function = (PyFunction)referenceTarget; - if (PyNames.NAMEDTUPLE.equals(function.getName()) && PyNames.COLLECTIONS_NAMEDTUPLE.equals(function.getQualifiedName())) { - return PyNamedTupleType.fromCall((PyCallExpression)anchor, context, 2); - } + return getNamedTupleTypeFromAST((PyCallExpression)anchor, context, 2); } return null; } @@ -321,4 +313,42 @@ public class PyStdlibTypeProvider extends PyTypeProviderBase { } return result; } + + @Nullable + private static PyType getNamedTupleTypeFromStub(@NotNull PsiElement referenceTarget, + @Nullable PyNamedTupleStub stub, + int definitionLevel) { + if (stub == null) { + return null; + } + + final PyClass tupleClass = PyBuiltinCache.getInstance(referenceTarget).getClass(PyNames.FAKE_NAMEDTUPLE); + if (tupleClass == null) { + return null; + } + + return new PyNamedTupleType(tupleClass, referenceTarget, stub.getName(), stub.getFields(), definitionLevel); + } + + @Nullable + private static PyType getNamedTupleTypeFromAST(@NotNull PyTargetExpression expression, + @NotNull TypeEvalContext context, + int definitionLevel) { + if (context.maySwitchToAST(expression)) { + return getNamedTupleTypeFromStub(expression, PyNamedTupleStubImpl.create(expression), definitionLevel); + } + + return null; + } + + @Nullable + private static PyType getNamedTupleTypeFromAST(@NotNull PyCallExpression expression, + @NotNull TypeEvalContext context, + int definitionLevel) { + if (context.maySwitchToAST(expression)) { + return getNamedTupleTypeFromStub(expression, PyNamedTupleStubImpl.create(expression), definitionLevel); + } + + return null; + } } diff --git a/python/src/com/jetbrains/python/psi/PyFileElementType.java b/python/src/com/jetbrains/python/psi/PyFileElementType.java index fb9f1b52eb68..c89e07f22a99 100644 --- a/python/src/com/jetbrains/python/psi/PyFileElementType.java +++ b/python/src/com/jetbrains/python/psi/PyFileElementType.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2016 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. @@ -62,7 +62,7 @@ public class PyFileElementType extends IStubFileElementType { @Override public int getStubVersion() { // Don't forget to update versions of indexes that use the updated stub-based elements - return 55; + return 56; } @Nullable diff --git a/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubImpl.java b/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubImpl.java new file mode 100644 index 000000000000..9e1398e1dd5b --- /dev/null +++ b/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubImpl.java @@ -0,0 +1,352 @@ +/* + * Copyright 2000-2016 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.psi.impl.stubs; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.stubs.StubInputStream; +import com.intellij.psi.stubs.StubOutputStream; +import com.intellij.psi.util.QualifiedName; +import com.intellij.util.io.StringRef; +import com.jetbrains.python.PyNames; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyPsiUtils; +import com.jetbrains.python.psi.resolve.PyResolveProcessor; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.stubs.PyNamedTupleStub; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class PyNamedTupleStubImpl implements PyNamedTupleStub { + + @Nullable + private final QualifiedName myCalleeName; + + @NotNull + private final String myName; + + @NotNull + private final List myFields; + + private PyNamedTupleStubImpl(@Nullable QualifiedName calleeName, @NotNull String name, @NotNull List fields) { + myCalleeName = calleeName; + myName = name; + myFields = Collections.unmodifiableList(new ArrayList<>(fields)); + } + + @Nullable + public static PyNamedTupleStub create(@NotNull PyTargetExpression expression) { + final PyExpression assignedValue = expression.findAssignedValue(); + + if (assignedValue instanceof PyCallExpression) { + return create((PyCallExpression)assignedValue); + } + + return null; + } + + @Nullable + public static PyNamedTupleStub create(@NotNull PyCallExpression expression) { + final PyReferenceExpression calleeReference = PyUtil.as(expression.getCallee(), PyReferenceExpression.class); + + if (calleeReference == null) { + return null; + } + + final QualifiedName namedTupleQName = getNamedTupleQName(calleeReference); + + if (namedTupleQName != null) { + final String name = resolveTupleName(expression); + + if (name == null) { + return null; + } + + final List fields = resolveTupleFields(expression); + + if (fields == null) { + return null; + } + + return new PyNamedTupleStubImpl(namedTupleQName, name, fields); + } + + return null; + } + + @Nullable + public static PyNamedTupleStub deserialize(@NotNull StubInputStream stream) throws IOException { + final StringRef calleeName = stream.readName(); + final StringRef name = stream.readName(); + final List fields = deserializeFields(stream, stream.readVarInt()); + + if (calleeName == null || name == null) { + return null; + } + + return new PyNamedTupleStubImpl( + QualifiedName.fromDottedString(calleeName.getString()), + name.getString(), + fields + ); + } + + @NotNull + @Override + public Class getTypeClass() { + return PyNamedTupleStubType.class; + } + + @Override + public void serialize(@NotNull StubOutputStream stream) throws IOException { + stream.writeName(myCalleeName == null ? null : myCalleeName.toString()); + stream.writeName(myName); + stream.writeVarInt(myFields.size()); + + for (String field : myFields) { + stream.writeName(field); + } + } + + @Nullable + @Override + public QualifiedName getCalleeName() { + return myCalleeName; + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @NotNull + @Override + public List getFields() { + return myFields; + } + + @Nullable + private static QualifiedName getNamedTupleQName(@NotNull PyReferenceExpression referenceExpression) { + final QualifiedName name = getFullyQualifiedNamedTupleQName(referenceExpression); + + if (name != null) { + return name; + } + + return getImportedNamedTupleQName(referenceExpression); + } + + @Nullable + private static String resolveTupleName(@NotNull PyCallExpression callExpression) { + // SUPPORTED CASES: + + // name = "Point" + // Point = namedtuple(name, ...) + + // Point = namedtuple("Point", ...) + + // Point = namedtuple(("Point"), ...) + + final PyExpression nameExpression = PyPsiUtils.flattenParens(callExpression.getArgument(0, PyExpression.class)); + + if (nameExpression instanceof PyReferenceExpression) { + return PyPsiUtils.strValue(fullResolveLocally((PyReferenceExpression)nameExpression)); + } + + return PyPsiUtils.strValue(nameExpression); + } + + @Nullable + private static List resolveTupleFields(@NotNull PyCallExpression callExpression) { + // SUPPORTED CASES: + + // fields = ["x", "y"] + // Point = namedtuple(..., fields) + + // Point = namedtuple(..., "x y") + + // Point = namedtuple(..., ("x y")) + + // Point = namedtuple(..., "x, y") + + // Point = namedtuple(..., ["x", "y"]) + + final PyExpression fieldsExpression = PyPsiUtils.flattenParens(callExpression.getArgument(1, PyExpression.class)); + + if (fieldsExpression instanceof PyReferenceExpression) { + return extractFields(fullResolveLocally((PyReferenceExpression)fieldsExpression)); + } + + return extractFields(fieldsExpression); + } + + @NotNull + private static List deserializeFields(@NotNull StubInputStream stream, int fieldsSize) throws IOException { + final List fields = new ArrayList<>(fieldsSize); + + for (int i = 0; i < fieldsSize; i++) { + final StringRef field = stream.readName(); + + if (field != null) { + fields.add(field.getString()); + } + } + + return fields; + } + + @Nullable + private static QualifiedName getFullyQualifiedNamedTupleQName(@NotNull PyReferenceExpression referenceExpression) { + // SUPPORTED CASES: + + // import collections + // Point = collections.namedtuple(...) + + // import collections as c + // Point = c.namedtuple(...) + + if (PyNames.NAMEDTUPLE.equals(referenceExpression.getName())) { + final PyExpression qualifier = referenceExpression.getQualifier(); + + if (qualifier instanceof PyReferenceExpression) { + final PyReferenceExpression qualifierReference = (PyReferenceExpression)qualifier; + + if (!qualifierReference.isQualified() && resolvesToCollections(qualifierReference)) { + return QualifiedName.fromComponents(qualifierReference.getName(), referenceExpression.getName()); + } + } + } + + return null; + } + + @Nullable + private static QualifiedName getImportedNamedTupleQName(@NotNull PyReferenceExpression referenceExpression) { + // SUPPORTED CASES: + + // from collections import namedtuple + // Point = namedtuple(...) + + // from collections import namedtuple as NT + // Point = NT(...) + + for (PsiElement element : resolveLocally(referenceExpression)) { + if (element instanceof PyImportElement) { + final PyImportElement importElement = (PyImportElement)element; + + if (equals(importElement.getImportedQName(), PyNames.NAMEDTUPLE)) { + final PyStatement importStatement = importElement.getContainingImportStatement(); + + if (importStatement instanceof PyFromImportStatement) { + final PyFromImportStatement fromImportStatement = (PyFromImportStatement)importStatement; + + if (equals(fromImportStatement.getImportSourceQName(), PyNames.COLLECTIONS)) { + return QualifiedName.fromComponents(referenceExpression.getName()); + } + } + } + } + } + + return null; + } + + private static boolean resolvesToCollections(@NotNull PyReferenceExpression referenceExpression) { + for (PsiElement element : resolveLocally(referenceExpression)) { + if (element instanceof PyImportElement) { + final PyImportElement importElement = (PyImportElement)element; + + if (equals(importElement.getImportedQName(), PyNames.COLLECTIONS)) { + return true; + } + } + } + + return false; + } + + @NotNull + private static Collection resolveLocally(@NotNull PyReferenceExpression referenceExpression) { + final String referenceName = referenceExpression.getName(); + + if (referenceName == null) { + return Collections.emptyList(); + } + + final PyResolveProcessor processor = new PyResolveProcessor(referenceName, true); + PyResolveUtil.scopeCrawlUp(processor, referenceExpression, referenceName, null); + + return processor.getElements(); + } + + private static boolean equals(@Nullable QualifiedName qualifiedName, @NotNull String name) { + return qualifiedName != null && name.equals(qualifiedName.toString()); + } + + @Nullable + private static PyExpression fullResolveLocally(@NotNull PyReferenceExpression referenceExpression) { + for (PsiElement element : resolveLocally(referenceExpression)) { + if (element instanceof PyTargetExpression) { + final PyExpression assignedValue = ((PyTargetExpression)element).findAssignedValue(); + + if (assignedValue instanceof PyReferenceExpression) { + return fullResolveLocally((PyReferenceExpression)assignedValue); + } + + return assignedValue; + } + } + + return null; + } + + @Nullable + private static List extractFields(@Nullable PyExpression expression) { + if (expression == null) { + return null; + } + + final List listValue = PyUtil.strListValue(expression); + + if (listValue != null) { + return listValue; + } + + return extractFields(PyPsiUtils.strValue(expression)); + } + + @Nullable + private static List extractFields(@Nullable String fieldsString) { + if (fieldsString == null) { + return null; + } + + final List result = new ArrayList(); + + for (String name : StringUtil.tokenize(fieldsString, ", ")) { + result.add(name); + } + + return result; + } +} diff --git a/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubType.java b/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubType.java new file mode 100644 index 000000000000..fa2d47709cfc --- /dev/null +++ b/python/src/com/jetbrains/python/psi/impl/stubs/PyNamedTupleStubType.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2016 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.psi.impl.stubs; + +import com.intellij.psi.stubs.StubInputStream; +import com.jetbrains.python.psi.PyTargetExpression; +import com.jetbrains.python.psi.stubs.PyNamedTupleStub; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class PyNamedTupleStubType extends CustomTargetExpressionStubType { + + @Nullable + @Override + public PyNamedTupleStub createStub(@NotNull PyTargetExpression psi) { + return PyNamedTupleStubImpl.create(psi); + } + + @Nullable + @Override + public PyNamedTupleStub deserializeStub(@NotNull StubInputStream stream) throws IOException { + return PyNamedTupleStubImpl.deserialize(stream); + } +} diff --git a/python/testData/stubs/FullyQualifiedNamedTuple.py b/python/testData/stubs/FullyQualifiedNamedTuple.py new file mode 100644 index 000000000000..c5537c6d6609 --- /dev/null +++ b/python/testData/stubs/FullyQualifiedNamedTuple.py @@ -0,0 +1,3 @@ +import collections + +nt = collections.namedtuple("name", "field") \ No newline at end of file diff --git a/python/testData/stubs/FullyQualifiedNamedTupleWithAs.py b/python/testData/stubs/FullyQualifiedNamedTupleWithAs.py new file mode 100644 index 000000000000..33fff2d2a1d8 --- /dev/null +++ b/python/testData/stubs/FullyQualifiedNamedTupleWithAs.py @@ -0,0 +1,3 @@ +import collections as C + +nt = C.namedtuple("name", "field") \ No newline at end of file diff --git a/python/testData/stubs/ImportedNamedTuple.py b/python/testData/stubs/ImportedNamedTuple.py new file mode 100644 index 000000000000..d00e565a24fc --- /dev/null +++ b/python/testData/stubs/ImportedNamedTuple.py @@ -0,0 +1,3 @@ +from collections import namedtuple + +nt = namedtuple("name", "field") \ No newline at end of file diff --git a/python/testData/stubs/ImportedNamedTupleFields.py b/python/testData/stubs/ImportedNamedTupleFields.py new file mode 100644 index 000000000000..5af2989d7fe7 --- /dev/null +++ b/python/testData/stubs/ImportedNamedTupleFields.py @@ -0,0 +1,4 @@ +from collections import namedtuple +from b import fields + +nt = namedtuple("name", fields) \ No newline at end of file diff --git a/python/testData/stubs/ImportedNamedTupleName.py b/python/testData/stubs/ImportedNamedTupleName.py new file mode 100644 index 000000000000..f82421dcf1a5 --- /dev/null +++ b/python/testData/stubs/ImportedNamedTupleName.py @@ -0,0 +1,4 @@ +from collections import namedtuple +from b import name + +nt = namedtuple(name, ["x", "y"]) \ No newline at end of file diff --git a/python/testData/stubs/ImportedNamedTupleWithAs.py b/python/testData/stubs/ImportedNamedTupleWithAs.py new file mode 100644 index 000000000000..0ce02241a2bf --- /dev/null +++ b/python/testData/stubs/ImportedNamedTupleWithAs.py @@ -0,0 +1,3 @@ +from collections import namedtuple as NT + +nt = NT("name", "field") \ No newline at end of file diff --git a/python/testData/stubs/NamedTupleFieldsChain.py b/python/testData/stubs/NamedTupleFieldsChain.py new file mode 100644 index 000000000000..077f5ff5386f --- /dev/null +++ b/python/testData/stubs/NamedTupleFieldsChain.py @@ -0,0 +1,5 @@ +from collections import namedtuple + +fields0 = ["x", "y"] +fields = fields0 +nt = namedtuple("name", fields) \ No newline at end of file diff --git a/python/testData/stubs/NamedTupleFieldsReference.py b/python/testData/stubs/NamedTupleFieldsReference.py new file mode 100644 index 000000000000..3cfccfd0b93a --- /dev/null +++ b/python/testData/stubs/NamedTupleFieldsReference.py @@ -0,0 +1,4 @@ +from collections import namedtuple + +fields = ["x", "y"] +nt = namedtuple("name", fields) \ No newline at end of file diff --git a/python/testData/stubs/NamedTupleFieldsSequence.py b/python/testData/stubs/NamedTupleFieldsSequence.py new file mode 100644 index 000000000000..3b070192fb1f --- /dev/null +++ b/python/testData/stubs/NamedTupleFieldsSequence.py @@ -0,0 +1,3 @@ +from collections import namedtuple + +nt = namedtuple("name", ["x", "y"]) \ No newline at end of file diff --git a/python/testData/stubs/NamedTupleNameChain.py b/python/testData/stubs/NamedTupleNameChain.py new file mode 100644 index 000000000000..b661181145db --- /dev/null +++ b/python/testData/stubs/NamedTupleNameChain.py @@ -0,0 +1,5 @@ +from collections import namedtuple + +name0 = "name" +name = name0 +nt = namedtuple(name, ["x", "y"]) \ No newline at end of file diff --git a/python/testData/stubs/NamedTupleNameReference.py b/python/testData/stubs/NamedTupleNameReference.py new file mode 100644 index 000000000000..f7d2042e35bd --- /dev/null +++ b/python/testData/stubs/NamedTupleNameReference.py @@ -0,0 +1,4 @@ +from collections import namedtuple + +name = "name" +nt = namedtuple(name, ["x", "y"]) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyStubsTest.java b/python/testSrc/com/jetbrains/python/PyStubsTest.java index fdc83b56871f..7d4005a09985 100644 --- a/python/testSrc/com/jetbrains/python/PyStubsTest.java +++ b/python/testSrc/com/jetbrains/python/PyStubsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 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. @@ -15,6 +15,7 @@ */ package com.jetbrains.python; +import com.intellij.lang.FileASTNode; import com.intellij.openapi.application.Result; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Document; @@ -27,19 +28,24 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubElement; import com.intellij.psi.util.QualifiedName; import com.intellij.testFramework.TestDataPath; +import com.jetbrains.python.codeInsight.stdlib.PyNamedTupleType; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyFileImpl; import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; import com.jetbrains.python.psi.stubs.PyClassNameIndex; +import com.jetbrains.python.psi.stubs.PyNamedTupleStub; import com.jetbrains.python.psi.stubs.PyVariableNameIndex; import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.toolbox.Maybe; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -459,10 +465,115 @@ public class PyStubsTest extends PyTestCase { final String annotation = target.getTypeCommentAnnotation(); assertEquals("int", annotation); assertNotParsed(file); - + final TypeEvalContext context = TypeEvalContext.codeAnalysis(myFixture.getProject(), file); final PyType paramType = context.getType(target); assertInstanceOf(paramType, PyClassType.class); assertNotParsed(file); } + + public void testFullyQualifiedNamedTuple() { + doTestNamedTuple( + QualifiedName.fromDottedString("collections.namedtuple") + ); + } + + public void testFullyQualifiedNamedTupleWithAs() { + doTestNamedTuple( + QualifiedName.fromDottedString("C.namedtuple") + ); + } + + public void testImportedNamedTuple() { + doTestNamedTuple( + QualifiedName.fromComponents("namedtuple") + ); + } + + public void testImportedNamedTupleWithAs() { + doTestNamedTuple( + QualifiedName.fromComponents("NT") + ); + } + + public void testNamedTupleFieldsSequence() { + doTestNamedTupleArguments(); + } + + public void testNamedTupleNameReference() { + doTestNamedTupleArguments(); + } + + public void testNamedTupleFieldsReference() { + doTestNamedTupleArguments(); + } + + public void testNamedTupleNameChain() { + doTestNamedTupleArguments(); + } + + public void testNamedTupleFieldsChain() { + doTestNamedTupleArguments(); + } + + public void testImportedNamedTupleName() { + doTestUnsupportedNamedTuple(); + } + + public void testImportedNamedTupleFields() { + doTestUnsupportedNamedTuple(); + } + + private void doTestNamedTuple(@NotNull QualifiedName expectedCalleeName) { + doTestNamedTuple("name", Collections.singletonList("field"), expectedCalleeName); + } + + private void doTestNamedTupleArguments() { + doTestNamedTuple("name", Arrays.asList("x", "y"), QualifiedName.fromComponents("namedtuple")); + } + + private void doTestNamedTuple(@NotNull String expectedName, + @NotNull List expectedFields, + @NotNull QualifiedName expectedCalleeName) { + final PyFile file = getTestFile(); + + final PyTargetExpression attribute = file.findTopLevelAttribute("nt"); + assertNotNull(attribute); + + final PyNamedTupleStub stub = attribute.getStub().getCustomStub(PyNamedTupleStub.class); + assertNotNull(stub); + assertEquals(expectedCalleeName, stub.getCalleeName()); + + final PyType typeFromStub = TypeEvalContext.codeInsightFallback(myFixture.getProject()).getType(attribute); + doTestNamedTuple(expectedName, expectedFields, typeFromStub); + assertNotParsed(file); + + final FileASTNode astNode = file.getNode(); + assertNotNull(astNode); + + final PyType typeFromAst = TypeEvalContext.userInitiated(myFixture.getProject(), file).getType(attribute); + doTestNamedTuple(expectedName, expectedFields, typeFromAst); + } + + private void doTestUnsupportedNamedTuple() { + final PyFile file = getTestFile(); + + final PyTargetExpression attribute = file.findTopLevelAttribute("nt"); + assertNotNull(attribute); + + final TypeEvalContext context = TypeEvalContext.codeInsightFallback(myFixture.getProject()); + + assertNull(context.getType(attribute)); + } + + private static void doTestNamedTuple(@NotNull String expectedName, + @NotNull List expectedFields, + @Nullable PyType type) { + assertInstanceOf(type, PyNamedTupleType.class); + + final PyNamedTupleType namedTupleType = (PyNamedTupleType)type; + + assertEquals(expectedName, namedTupleType.getName()); + assertEquals(expectedFields, namedTupleType.getElementNames()); + } }