From bc0940e05d1dc6e402168f2a4ac145c2b1fd1e05 Mon Sep 17 00:00:00 2001 From: Morgan Bartholomew Date: Tue, 19 Aug 2025 18:54:48 +1000 Subject: [PATCH] PY-84033 type engine: callable representation parsing GitOrigin-RevId: c2926c5d507c2380af394593785297d0a5ddad18 --- .../resources/intellij.python.psi.impl.xml | 4 + .../resources/messages/PyPsiBundle.properties | 1 + .../PyTypeRepresentationDialect.kt | 27 ++ .../PyTypeRepresentationElementTypes.kt | 30 ++ .../PyTypeRepresentationFileElementType.kt | 22 ++ .../PyTypeRepresentationFileType.kt | 19 ++ .../PyTypeRepresentationParser.kt | 263 ++++++++++++++++++ .../PyTypeRepresentationParserDefinition.kt | 35 +++ .../psi/PyFunctionTypeRepresentation.kt | 111 ++++++++ .../psi/PyNamedParameterTypeRepresentation.kt | 46 +++ .../psi/PyParameterListRepresentation.kt | 14 + .../psi/PyTypeRepresentationFile.kt | 50 ++++ .../typing/PyTypingTypeProvider.java | 13 +- .../parsing/ callable complex varargs.txt | 42 +++ .../parsing/ callable dangling comma.txt | 17 ++ .../parsing/ callable default parameter.txt | 23 ++ .../parsing/ callable ellipsis.txt | 14 + .../parsing/ callable empty function type.txt | 2 + .../parsing/ callable empty.txt | 10 + .../parsing/ callable named parameter.txt | 16 ++ .../parsing/ callable nested.txt | 38 +++ .../parsing/ callable no return type.txt | 10 + .../parsing/ callable no type after star.txt | 16 ++ .../parsing/ callable simple.txt | 16 ++ .../parsing/ callable varargs.txt | 20 ++ .../parsing/ todo in callable.txt | 24 ++ .../typeRepresentation/parsing/ todo.txt | 15 + .../parsing/CallableDanglingComma.txt | 17 ++ .../parsing/CallableDefAsFirstType.txt | 14 + .../parsing/CallableEllipsis.txt | 14 + .../parsing/CallableEmpty.txt | 2 + .../parsing/CallableEmptyFunctionType.txt | 2 + .../parsing/CallableLambdaAsFirstType.txt | 13 + .../parsing/CallableNoArrowAndReturnType.txt | 6 + .../parsing/CallableNoClosingParenthesis.txt | 7 + .../parsing/CallableNoReturnType.txt | 10 + .../parsing/CallableNoTypeAfterStar.txt | 16 ++ .../PyFunctionTypeAnnotationParsingTest.java | 2 +- .../python/PyTypeRepresentationParsingTest.kt | 156 +++++++++++ 39 files changed, 1154 insertions(+), 3 deletions(-) create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt create mode 100644 python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt create mode 100644 python/testData/typeRepresentation/parsing/ callable complex varargs.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable dangling comma.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable default parameter.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable ellipsis.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable empty function type.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable empty.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable named parameter.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable nested.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable no return type.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable no type after star.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable simple.txt create mode 100644 python/testData/typeRepresentation/parsing/ callable varargs.txt create mode 100644 python/testData/typeRepresentation/parsing/ todo in callable.txt create mode 100644 python/testData/typeRepresentation/parsing/ todo.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableDanglingComma.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableEllipsis.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableEmpty.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableNoReturnType.txt create mode 100644 python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt create mode 100644 python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt diff --git a/python/python-psi-impl/resources/intellij.python.psi.impl.xml b/python/python-psi-impl/resources/intellij.python.psi.impl.xml index f3a5392c542a..4b53abe84d31 100644 --- a/python/python-psi-impl/resources/intellij.python.psi.impl.xml +++ b/python/python-psi-impl/resources/intellij.python.psi.impl.xml @@ -93,6 +93,10 @@ + + + diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties index 1825ca92b96d..5d5d523e75ea 100644 --- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties +++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties @@ -516,6 +516,7 @@ ACT.from.some.module.import=Import from\u2026 filetype.python.docstring.description=Python docstring filetype.python.function.type.annotation.description=Python PEP-484 function type comment +filetype.python.type.representation.description=Type representation language filetype.python.type.hint.description=Python PEP-484 type hint python.docstring.format=Docstring format: python.docstring.select.type=Select Docstring Type diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt new file mode 100644 index 000000000000..8b71aff2a999 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationDialect.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation + +import com.intellij.lang.DependentLanguage +import com.intellij.lang.Language +import com.jetbrains.python.PythonLanguage + +/** + * Used to represent types that are not possible to express in the language like callable types + * + * This is used to serialize types, and for type engine communication + */ +object PyTypeRepresentationDialect : Language(PythonLanguage.getInstance(), "PyTypeRepresentation"), DependentLanguage diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt new file mode 100644 index 000000000000..12fee0b8459e --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationElementTypes.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation + +import com.intellij.psi.tree.IElementType +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyNamedParameterTypeRepresentation +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyParameterListRepresentation +import com.jetbrains.python.psi.PyElementType +import com.jetbrains.python.psi.impl.PyElementImpl + +object PyTypeRepresentationElementTypes { + val FUNCTION_SIGNATURE: PyElementType = PyElementType("FUNCTION_SIGNATURE") { node -> PyFunctionTypeRepresentation(node) } + val PARAMETER_TYPE_LIST: PyElementType = PyElementType("PARAMETER_TYPE_LIST") { node -> PyParameterListRepresentation(node) } + val NAMED_PARAMETER_TYPE: PyElementType = PyElementType("NAMED_PARAMETER_TYPE") { node -> PyNamedParameterTypeRepresentation(node) } + val PLACEHOLDER: IElementType = PyElementType("PLACEHOLDER") { node -> PyElementImpl(node) } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt new file mode 100644 index 000000000000..94f8d7f4b13f --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileElementType.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation + +import com.jetbrains.python.psi.PyFileElementType + +object PyTypeRepresentationFileElementType : PyFileElementType(PyTypeRepresentationDialect) { + override fun getExternalId(): String = "PyTypeRepresentation.ID" +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt new file mode 100644 index 000000000000..29b1511690e4 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationFileType.kt @@ -0,0 +1,19 @@ +package com.jetbrains.python.codeInsight.typeRepresentation + +import com.jetbrains.python.PyPsiBundle +import com.jetbrains.python.PythonFileType +import org.jetbrains.annotations.NonNls + +object PyTypeRepresentationFileType : PythonFileType(PyTypeRepresentationDialect) { + override fun getName(): @NonNls String { + return "PythonTypeRepresentation" + } + + override fun getDescription(): String { + return PyPsiBundle.message("filetype.python.type.representation.description") + } + + override fun getDefaultExtension(): String { + return "pythonTypeRepresentation" + } +} \ No newline at end of file diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt new file mode 100644 index 000000000000..a5a67b534011 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParser.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation + +import com.intellij.lang.SyntaxTreeBuilder +import com.intellij.openapi.util.NlsContexts +import com.intellij.psi.tree.IElementType +import com.jetbrains.python.PyElementTypes +import com.jetbrains.python.PyParsingBundle +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.parsing.ExpressionParsing +import com.jetbrains.python.parsing.ParsingContext +import com.jetbrains.python.parsing.PyParser +import com.jetbrains.python.parsing.StatementParsing +import com.jetbrains.python.psi.LanguageLevel + +class PyTypeRepresentationParser : PyParser() { + override fun createParsingContext(builder: SyntaxTreeBuilder?, languageLevel: LanguageLevel?): ParsingContext { + return object : ParsingContext(builder, languageLevel) { + private val myExpressionParsing = TypeRepresentationParser(this) + private val myStatementParsing = object : StatementParsing(this) { + override fun parseStatement() { + expressionParser.parseExpression() + } + } + + override fun getExpressionParser(): ExpressionParsing = myExpressionParsing + override fun getStatementParser(): StatementParsing = myStatementParsing + } + } + + private class TypeRepresentationParser(context: ParsingContext?) : ExpressionParsing(context) { + override fun parseExpression() { + if (myBuilder.eof()) { + return + } + // Skip statement breaks - they're not part of type expressions + if (atToken(PyTokenTypes.STATEMENT_BREAK)) { + myBuilder.advanceLexer() + return + } + + val startOffset = myBuilder.currentOffset + + // Handle @Todo(...) annotations for unsupported type constructs + if (atToken(PyTokenTypes.AT)) { + parseTodoAnnotation() + return + } + + // Try to parse function type (callable syntax) first + if (!parseFunctionType()) { + // Not a function type, parse as regular expression + super.parseSingleExpression(false) + // Ensure we made progress + if (myBuilder.currentOffset == startOffset && !myBuilder.eof()) { + // We didn't consume anything, advance to avoid infinite loop + myBuilder.error("Unexpected token") // NON-NLS + myBuilder.advanceLexer() + } + } + } + + /** + * Parses @Todo(...) style annotations used to mark unsupported type constructs. + * These are opaque markers and their content is not interpreted. + */ + private fun parseTodoAnnotation() { + assert(atToken(PyTokenTypes.AT)) + val annotationMarker = myBuilder.mark() + myBuilder.advanceLexer() // consume @ + + // Consume tokens until we hit a structural boundary + var depth = 0 + while (!myBuilder.eof()) { + val token = myBuilder.getTokenType() + when { + token == PyTokenTypes.LPAR -> { + depth++ + myBuilder.advanceLexer() + } + token == PyTokenTypes.RPAR -> { + if (depth == 0) break // Structural boundary (parameter list close, etc.) + depth-- + myBuilder.advanceLexer() + } + token == PyTokenTypes.COMMA && depth == 0 -> break // Structural boundary + token == PyTokenTypes.RARROW && depth == 0 -> break // Structural boundary (->) + token == PyTokenTypes.STATEMENT_BREAK -> break // Structural boundary + else -> myBuilder.advanceLexer() + } + } + annotationMarker.done(PyTypeRepresentationElementTypes.PLACEHOLDER) + } + + fun parseFunctionType(): Boolean { + if (atToken(PyTokenTypes.LPAR)) { + val mark = myBuilder.mark() + parseParameterTypeList() + // Check if this is a function type (has ->) or just a parenthesized expression/tuple + if (atToken(PyTokenTypes.RARROW)) { + myBuilder.advanceLexer() // consume -> + // Use parseExpression to handle nested function types in return position + parseExpression() + mark.done(PyTypeRepresentationElementTypes.FUNCTION_SIGNATURE) + return true + } + else { + // Not a function type, rollback + mark.rollbackTo() + return false + } + } + return false + } + + fun parseParameterTypeList() { + assert(atToken(PyTokenTypes.LPAR)) + val listMark = myBuilder.mark() + myBuilder.advanceLexer() + + var paramCount = 0 + while (!(atAnyOfTokens(PyTokenTypes.RPAR, PyTokenTypes.RARROW, PyTokenTypes.STATEMENT_BREAK) || myBuilder.eof())) { + val currentOffset = myBuilder.currentOffset + + if (paramCount > 0) { + if (!checkMatches(PyTokenTypes.COMMA, PyParsingBundle.message("PARSE.expected.comma"))) { + // No comma found, break out of parameter parsing + break + } + } + val parsed: Boolean + if (atToken(PyTokenTypes.DIV)) { + // Handle positional-only separator / + val slashMarker = myBuilder.mark() + myBuilder.advanceLexer() + slashMarker.done(PyElementTypes.SLASH_PARAMETER) + parsed = true + } + else if (atToken(PyTokenTypes.MULT)) { + val starMarker = myBuilder.mark() + myBuilder.advanceLexer() + // Check if this is a named *args parameter (*name: type) + if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) { + val namedVarargMarker = myBuilder.mark() + myBuilder.advanceLexer() // consume identifier + myBuilder.advanceLexer() // consume colon + // Parse the type expression, handling * prefix + if (atToken(PyTokenTypes.MULT)) { + val innerStarMarker = myBuilder.mark() + myBuilder.advanceLexer() + parseExpression() + innerStarMarker.done(PyElementTypes.STAR_EXPRESSION) + } + else { + parseExpression() + } + namedVarargMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE) + } + else { + // Unnamed *args, just parse the type + parseExpression() + } + parsed = true + starMarker.done(PyElementTypes.STAR_EXPRESSION) + } + else if (atToken(PyTokenTypes.EXP)) { + val doubleStarMarker = myBuilder.mark() + myBuilder.advanceLexer() + // Check if this is a named **kwargs parameter (**name: type) + if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) { + val namedKwargMarker = myBuilder.mark() + myBuilder.advanceLexer() // consume identifier + myBuilder.advanceLexer() // consume colon + // Parse the type expression, handling ** prefix + if (atToken(PyTokenTypes.EXP)) { + val innerDoubleStarMarker = myBuilder.mark() + myBuilder.advanceLexer() + parseExpression() + innerDoubleStarMarker.done(PyElementTypes.DOUBLE_STAR_EXPRESSION) + } + else { + parseExpression() + } + namedKwargMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE) + } + else { + // Unnamed **kwargs, just parse the type + parseExpression() + } + parsed = true + doubleStarMarker.done(PyElementTypes.DOUBLE_STAR_EXPRESSION) + } + else { + if (atToken(PyTokenTypes.AT)) { + parseTodoAnnotation() + parsed = true + } + // Check if this is a named parameter (name: type) or (name: type = default) + else if (atToken(PyTokenTypes.IDENTIFIER) && myBuilder.lookAhead(1) == PyTokenTypes.COLON) { + val namedParamMarker = myBuilder.mark() + myBuilder.advanceLexer() // consume identifier + myBuilder.advanceLexer() // consume colon + // Parse the type expression + parseExpression() + // Check for default value + if (atToken(PyTokenTypes.EQ)) { + myBuilder.advanceLexer() // consume = + parseExpression() // parse default value + } + namedParamMarker.done(PyTypeRepresentationElementTypes.NAMED_PARAMETER_TYPE) + parsed = myBuilder.currentOffset > currentOffset + } + else { + // Use parseExpression to handle nested function types and positional types + parseExpression() + parsed = myBuilder.currentOffset > currentOffset // Check we made progress + } + } + if (!parsed) { + myBuilder.error(PyParsingBundle.message("PARSE.expected.expression")) + recoverUntilMatches(PyParsingBundle.message("PARSE.expected.expression"), PyTokenTypes.COMMA, PyTokenTypes.RPAR, + PyTokenTypes.RARROW, PyTokenTypes.STATEMENT_BREAK) + } + paramCount++ + + } + checkMatches(PyTokenTypes.RPAR, PyParsingBundle.message("PARSE.expected.rpar")) + listMark.done(PyTypeRepresentationElementTypes.PARAMETER_TYPE_LIST) + } + + fun recoverUntilMatches(errorMessage: @NlsContexts.ParsingError String, vararg types: IElementType) { + val errorMarker = myBuilder.mark() + var hasNonWhitespaceTokens = false + while (!(atAnyOfTokens(*types) || myBuilder.eof())) { + // Regular whitespace tokens are already skipped by advancedLexer() + if (!atToken(PyTokenTypes.STATEMENT_BREAK)) { + hasNonWhitespaceTokens = true + } + myBuilder.advanceLexer() + } + if (hasNonWhitespaceTokens) { + errorMarker.error(errorMessage) + } + else { + errorMarker.drop() + } + } + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt new file mode 100644 index 000000000000..4ba7934ec8d0 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/PyTypeRepresentationParserDefinition.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation + +import com.intellij.lang.PsiParser +import com.intellij.openapi.project.Project +import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IFileElementType +import com.intellij.psi.tree.TokenSet +import com.jetbrains.python.PythonParserDefinition +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyTypeRepresentationFile + +class PyTypeRepresentationParserDefinition : PythonParserDefinition() { + override fun getCommentTokens(): TokenSet = TokenSet.EMPTY + + override fun createFile(viewProvider: FileViewProvider): PsiFile = PyTypeRepresentationFile(viewProvider) + + override fun getFileNodeType(): IFileElementType = PyTypeRepresentationFileElementType + + override fun createParser(project: Project?): PsiParser = PyTypeRepresentationParser() +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt new file mode 100644 index 000000000000..00e4c4d56016 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyFunctionTypeRepresentation.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation.psi + +import com.intellij.lang.ASTNode +import com.jetbrains.python.ast.findChildByClass +import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider +import com.jetbrains.python.psi.PyDoubleStarExpression +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.PySlashParameter +import com.jetbrains.python.psi.PyStarExpression +import com.jetbrains.python.psi.impl.PyBuiltinCache +import com.jetbrains.python.psi.impl.PyElementImpl +import com.jetbrains.python.psi.types.* + +class PyFunctionTypeRepresentation(astNode: ASTNode) : PyElementImpl(astNode), PyExpression { + val parameterList: PyParameterListRepresentation + get() = findNotNullChildByClass(PyParameterListRepresentation::class.java) + + val returnType: PyExpression? + get() = findChildByClass(PyExpression::class.java) + + override fun getType(context: TypeEvalContext, key: TypeEvalContext.Key): PyType? { + val returnType = returnType ?: return null + val params = parameterList.parameters + val callableParams = params.map { param -> + when (param) { + is PySlashParameter -> { + // Positional-only separator + PyCallableParameterImpl.psi(param) + } + is PyNamedParameterTypeRepresentation -> { + val paramName = param.parameterName + val paramType = param.typeExpression?.let { resolveTypeExpression(it, context) } + PyCallableParameterImpl.nonPsi(paramName, paramType, param.defaultValue) + } + is PyStarExpression -> { + // *args parameter + // Check if it contains a named parameter + val namedParam = param.findChildByClass(PyNamedParameterTypeRepresentation::class.java) + if (namedParam != null) { + // *args: type + val paramName = namedParam.parameterName + val paramType = namedParam.typeExpression?.let { resolveTypeExpression(it, context) } + PyCallableParameterImpl.positionalNonPsi(paramName, paramType) + } + else { + // Unnamed *args: *type + val innerExpr = param.expression + val paramType = innerExpr?.let { resolveTypeExpression(it, context) } + PyCallableParameterImpl.positionalNonPsi(null, paramType) + } + } + is PyDoubleStarExpression -> { + // **kwargs parameter + // Check if it contains a named parameter + val namedParam = param.findChildByClass(PyNamedParameterTypeRepresentation::class.java) + if (namedParam != null) { + val paramName = namedParam.parameterName + val paramType = namedParam.typeExpression?.let { + if (it is PyDoubleStarExpression) + // Named kwargs unpacked: `**name: **type` + resolveTypeExpression(it.expression!!, context) + else { + // Named kwargs: `**name: type`, adapt to `dict` + val builtins = PyBuiltinCache.getInstance(it) + PyCollectionTypeImpl( + builtins.dictType!!.pyClass, false, listOf(builtins.strType, resolveTypeExpression(it, context)) + ) + } + } + PyCallableParameterImpl.keywordNonPsi(paramName, paramType) + } + else { + // Unnamed kwargs: `**type` + val innerExpr = param.expression + val paramType = innerExpr?.let { resolveTypeExpression(it, context) } + PyCallableParameterImpl.keywordNonPsi(null, paramType) + } + } + is PyExpression -> { + val paramType = resolveTypeExpression(param, context) + PyCallableParameterImpl.nonPsi(paramType) + } + else -> { + PyCallableParameterImpl.nonPsi(null) + } + } + } + val retType = resolveTypeExpression(returnType, context) + return PyCallableTypeImpl(callableParams, retType) + } + + private fun resolveTypeExpression(expr: PyExpression, context: TypeEvalContext): PyType? = when (expr) { + is PyDoubleStarExpression -> PyTypingTypeProvider.getType(expr.expression!!, context)?.get() + else -> PyTypingTypeProvider.getType(expr, context)?.get() + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt new file mode 100644 index 000000000000..06a79b3a2c2e --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyNamedParameterTypeRepresentation.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2025 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.codeInsight.typeRepresentation.psi + +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.impl.PyElementImpl + +class PyNamedParameterTypeRepresentation(astNode: ASTNode) : PyElementImpl(astNode), PsiElement { + val parameterName: String? + get() = node.findChildByType(PyTokenTypes.IDENTIFIER)?.text + + val typeExpression: PyExpression? + get() = findChildByClass(PyExpression::class.java) + + val defaultValue: PyExpression? + get() { + // Find the expression after the = token + val children = node.getChildren(null) + var foundEquals = false + for (child in children) { + if (foundEquals && child.psi is PyExpression) { + return child.psi as PyExpression + } + if (child.elementType == PyTokenTypes.EQ) { + foundEquals = true + } + } + return null + } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt new file mode 100644 index 000000000000..24d3c3665770 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyParameterListRepresentation.kt @@ -0,0 +1,14 @@ +package com.jetbrains.python.codeInsight.typeRepresentation.psi + +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationElementTypes +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.PySlashParameter +import com.jetbrains.python.psi.impl.PyElementImpl + +class PyParameterListRepresentation(astNode: ASTNode) : PyElementImpl(astNode) { + val parameters: List + get() = children.filter { it is PyExpression || it is PySlashParameter || it is PyNamedParameterTypeRepresentation || it.elementType == PyTypeRepresentationElementTypes.PLACEHOLDER } +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt new file mode 100644 index 000000000000..5d6d80438c1f --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typeRepresentation/psi/PyTypeRepresentationFile.kt @@ -0,0 +1,50 @@ +package com.jetbrains.python.codeInsight.typeRepresentation.psi + +import com.intellij.openapi.fileTypes.FileType +import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.impl.PsiManagerEx +import com.intellij.testFramework.LightVirtualFile +import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationDialect +import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationFileType +import com.jetbrains.python.psi.LanguageLevel +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.PyExpressionCodeFragment +import com.jetbrains.python.psi.impl.PyFileImpl + +class PyTypeRepresentationFile : PyFileImpl, PyExpressionCodeFragment { + + constructor(text: String, context: PsiElement) : super( + PsiManagerEx.getInstanceEx(context.project) + .fileManager + .createFileViewProvider( + LightVirtualFile( + "foo.bar", + PyTypeRepresentationFileType, + // workaround for PY-86754: strip def types + text.replace(Regex("(?:^|[^\"'])def [\\w.]+"), "") + ), + false) + ) { + myContext = context + } + + constructor(viewProvider: FileViewProvider) : super(viewProvider, PyTypeRepresentationDialect) { + myContext = null + } + + private val myContext: PsiElement? + + override fun getFileType(): FileType = PyTypeRepresentationFileType + + override fun toString(): String = "TypeRepresentation:$name" + + override fun getLanguageLevel(): LanguageLevel = + // The same as for .pyi files + LanguageLevel.getLatest() + + override fun getContext(): PsiElement? = if (myContext?.isValid == true) myContext else super.getContext() + + val type: PyExpression? + get() = findChildByClass(PyExpression::class.java) +} diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java index 7cbd653f05c0..7e7242d33523 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.java @@ -29,6 +29,7 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotation; import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeAnnotationFile; import com.jetbrains.python.codeInsight.typeHints.PyTypeHintFile; +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyEvaluator; @@ -1018,8 +1019,16 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext< if (classType != null) { return classType; } - if (context.myUseFqn && resolved.getText().equals("Unknown")) { - return Ref.create(); + if (context.myUseFqn) { + if (resolved.getText().equals("Unknown")) { + return Ref.create(); + } + if (resolved instanceof PyFunctionTypeRepresentation function) { + var result = context.myContext.getType(function); + if (result != null) { + return Ref.create(result); + } + } } return null; } diff --git a/python/testData/typeRepresentation/parsing/ callable complex varargs.txt b/python/testData/typeRepresentation/parsing/ callable complex varargs.txt new file mode 100644 index 000000000000..8d85bb849d7d --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable complex varargs.txt @@ -0,0 +1,42 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyStarExpression + PsiElement(Py:MULT)('*') + PyNamedParameterTypeRepresentation + PsiElement(Py:IDENTIFIER)('a') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PyStarExpression + PsiElement(Py:MULT)('*') + PySubscriptionExpression + PyReferenceExpression: tuple + PsiElement(Py:IDENTIFIER)('tuple') + PsiElement(Py:LBRACKET)('[') + PyTupleExpression + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: str + PsiElement(Py:IDENTIFIER)('str') + PsiElement(Py:RBRACKET)(']') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyDoubleStarExpression + PsiElement(Py:EXP)('**') + PyNamedParameterTypeRepresentation + PsiElement(Py:IDENTIFIER)('b') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PyDoubleStarExpression + PsiElement(Py:EXP)('**') + PyReferenceExpression: B + PsiElement(Py:IDENTIFIER)('B') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable dangling comma.txt b/python/testData/typeRepresentation/parsing/ callable dangling comma.txt new file mode 100644 index 000000000000..1f53dcfa25d8 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable dangling comma.txt @@ -0,0 +1,17 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiErrorElement:Unexpected token + + PsiElement(Py:RPAR)(')') + PsiErrorElement:')' expected + + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable default parameter.txt b/python/testData/typeRepresentation/parsing/ callable default parameter.txt new file mode 100644 index 000000000000..ab66bb97961a --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable default parameter.txt @@ -0,0 +1,23 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyNamedParameterTypeRepresentation + PsiElement(Py:IDENTIFIER)('a') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyEllipsisLiteralExpression + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable ellipsis.txt b/python/testData/typeRepresentation/parsing/ callable ellipsis.txt new file mode 100644 index 000000000000..3c308974d0c3 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable ellipsis.txt @@ -0,0 +1,14 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyEllipsisLiteralExpression + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable empty function type.txt b/python/testData/typeRepresentation/parsing/ callable empty function type.txt new file mode 100644 index 000000000000..4023f7ca065f --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable empty function type.txt @@ -0,0 +1,2 @@ +TypeRepresentation:a.pythonTypeRepresentation + \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable empty.txt b/python/testData/typeRepresentation/parsing/ callable empty.txt new file mode 100644 index 000000000000..1be1186b369e --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable empty.txt @@ -0,0 +1,10 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable named parameter.txt b/python/testData/typeRepresentation/parsing/ callable named parameter.txt new file mode 100644 index 000000000000..9dadbb74e9dc --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable named parameter.txt @@ -0,0 +1,16 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyNamedParameterTypeRepresentation + PsiElement(Py:IDENTIFIER)('a') + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable nested.txt b/python/testData/typeRepresentation/parsing/ callable nested.txt new file mode 100644 index 000000000000..e02ad5b98b5b --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable nested.txt @@ -0,0 +1,38 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: str + PsiElement(Py:IDENTIFIER)('str') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: str + PsiElement(Py:IDENTIFIER)('str') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable no return type.txt b/python/testData/typeRepresentation/parsing/ callable no return type.txt new file mode 100644 index 000000000000..1c3e033c23dc --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable no return type.txt @@ -0,0 +1,10 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable no type after star.txt b/python/testData/typeRepresentation/parsing/ callable no type after star.txt new file mode 100644 index 000000000000..b250a148d189 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable no type after star.txt @@ -0,0 +1,16 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyStarExpression + PsiElement(Py:MULT)('*') + PsiErrorElement:Unexpected token + + PsiElement(Py:RPAR)(')') + PsiErrorElement:')' expected + + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable simple.txt b/python/testData/typeRepresentation/parsing/ callable simple.txt new file mode 100644 index 000000000000..55cb459437ec --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable simple.txt @@ -0,0 +1,16 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyReferenceExpression: str + PsiElement(Py:IDENTIFIER)('str') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ callable varargs.txt b/python/testData/typeRepresentation/parsing/ callable varargs.txt new file mode 100644 index 000000000000..d617936f637b --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ callable varargs.txt @@ -0,0 +1,20 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyStarExpression + PsiElement(Py:MULT)('*') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyDoubleStarExpression + PsiElement(Py:EXP)('**') + PyReferenceExpression: str + PsiElement(Py:IDENTIFIER)('str') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ todo in callable.txt b/python/testData/typeRepresentation/parsing/ todo in callable.txt new file mode 100644 index 000000000000..720f38b9eec3 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ todo in callable.txt @@ -0,0 +1,24 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterListRepresentation + PsiElement(Py:LPAR)('(') + PyElement + PsiElement(Py:AT)('@') + PsiElement(Py:IDENTIFIER)('Todo') + PsiElement(Py:LPAR)('(') + PsiElement(Py:TICK)('`') + PsiElement(Py:IDENTIFIER)('Unpack') + PsiElement(Py:LBRACKET)('[') + PsiElement(Py:RBRACKET)(']') + PsiElement(Py:TICK)('`') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('special') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('form') + PsiElement(Py:RPAR)(')') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/ todo.txt b/python/testData/typeRepresentation/parsing/ todo.txt new file mode 100644 index 000000000000..f41a0d1de441 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/ todo.txt @@ -0,0 +1,15 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyElement + PsiElement(Py:AT)('@') + PsiElement(Py:IDENTIFIER)('Todo') + PsiElement(Py:LPAR)('(') + PsiElement(Py:TICK)('`') + PsiElement(Py:IDENTIFIER)('Unpack') + PsiElement(Py:LBRACKET)('[') + PsiElement(Py:RBRACKET)(']') + PsiElement(Py:TICK)('`') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('special') + PsiWhiteSpace(' ') + PsiElement(Py:IDENTIFIER)('form') + PsiElement(Py:RPAR)(')') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt b/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt new file mode 100644 index 000000000000..cdaee9445fe0 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableDanglingComma.txt @@ -0,0 +1,17 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterTypeListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:COMMA)(',') + PsiErrorElement:Unexpected token + + PsiElement(Py:RPAR)(')') + PsiErrorElement:')' expected + + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt b/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt new file mode 100644 index 000000000000..62b1686fb7f9 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableDefAsFirstType.txt @@ -0,0 +1,14 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PsiErrorElement:')' expected + + PsiErrorElement:Unexpected token + + PsiElement(Py:DEF_KEYWORD)('def') + PsiWhiteSpace(' ') + PyReferenceExpression: foo + PsiElement(Py:IDENTIFIER)('foo') + PsiErrorElement:Unexpected token + + PsiElement(Py:RPAR)(')') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableEllipsis.txt b/python/testData/typeRepresentation/parsing/CallableEllipsis.txt new file mode 100644 index 000000000000..a4be21b9e563 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableEllipsis.txt @@ -0,0 +1,14 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterTypeListRepresentation + PsiElement(Py:LPAR)('(') + PyEllipsisLiteralExpression + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:DOT)('.') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyNoneLiteralExpression + PsiElement(Py:NONE_KEYWORD)('None') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableEmpty.txt b/python/testData/typeRepresentation/parsing/CallableEmpty.txt new file mode 100644 index 000000000000..d0377053e877 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableEmpty.txt @@ -0,0 +1,2 @@ +TypeRepresentation:a.typeRepresentation + \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt b/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt new file mode 100644 index 000000000000..4023f7ca065f --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableEmptyFunctionType.txt @@ -0,0 +1,2 @@ +TypeRepresentation:a.pythonTypeRepresentation + \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt b/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt new file mode 100644 index 000000000000..6ddd1e6f0be2 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableLambdaAsFirstType.txt @@ -0,0 +1,13 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyLambdaExpression + PsiElement(Py:LAMBDA_KEYWORD)('lambda') + PyParameterList + + PsiElement(Py:COLON)(':') + PsiWhiteSpace(' ') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('42') + PsiErrorElement:')' expected + \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt b/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt new file mode 100644 index 000000000000..c722c09d25e6 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableNoArrowAndReturnType.txt @@ -0,0 +1,6 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:RPAR)(')') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt b/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt new file mode 100644 index 000000000000..433750e788da --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableNoClosingParenthesis.txt @@ -0,0 +1,7 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyParenthesizedExpression + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiErrorElement:')' expected + \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt b/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt new file mode 100644 index 000000000000..053290075166 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableNoReturnType.txt @@ -0,0 +1,10 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterTypeListRepresentation + PsiElement(Py:LPAR)('(') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') + PsiElement(Py:RPAR)(')') + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') \ No newline at end of file diff --git a/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt b/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt new file mode 100644 index 000000000000..daae37a6b760 --- /dev/null +++ b/python/testData/typeRepresentation/parsing/CallableNoTypeAfterStar.txt @@ -0,0 +1,16 @@ +TypeRepresentation:a.pythonTypeRepresentation + PyFunctionTypeRepresentation + PyParameterTypeListRepresentation + PsiElement(Py:LPAR)('(') + PyStarExpression + PsiElement(Py:MULT)('*') + PsiErrorElement:Unexpected token + + PsiElement(Py:RPAR)(')') + PsiErrorElement:')' expected + + PsiWhiteSpace(' ') + PsiElement(Py:RARROW)('->') + PsiWhiteSpace(' ') + PyReferenceExpression: int + PsiElement(Py:IDENTIFIER)('int') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java b/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java index f6b11af7a58e..ea6809f6443e 100644 --- a/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java +++ b/python/testSrc/com/jetbrains/python/PyFunctionTypeAnnotationParsingTest.java @@ -10,7 +10,6 @@ import com.jetbrains.python.codeInsight.functionTypeComments.psi.PyFunctionTypeA import com.jetbrains.python.documentation.doctest.PyDocstringTokenSetContributor; import com.jetbrains.python.psi.PyEllipsisLiteralExpression; import com.jetbrains.python.psi.PyExpression; -import com.jetbrains.python.psi.PyNoneLiteralExpression; import com.jetbrains.python.psi.PythonVisitorFilter; import com.jetbrains.python.psi.impl.PythonASTFactory; import org.jetbrains.annotations.NotNull; @@ -30,6 +29,7 @@ public class PyFunctionTypeAnnotationParsingTest extends ParsingTestCase { @Override protected void setUp() throws Exception { super.setUp(); + getApplication().registerService(PyElementTypesFacade.class, PyElementTypesFacadeImpl.class); registerExtensionPoint(PythonDialectsTokenSetContributor.EP_NAME, PythonDialectsTokenSetContributor.class); registerExtension(PythonDialectsTokenSetContributor.EP_NAME, new PythonTokenSetContributor()); registerExtension(PythonDialectsTokenSetContributor.EP_NAME, new PyDocstringTokenSetContributor()); diff --git a/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt b/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt new file mode 100644 index 000000000000..d01bb482442d --- /dev/null +++ b/python/testSrc/com/jetbrains/python/PyTypeRepresentationParsingTest.kt @@ -0,0 +1,156 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python + +import com.intellij.lang.LanguageASTFactory +import com.intellij.testFramework.ParsingTestCase +import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationDialect +import com.jetbrains.python.codeInsight.typeRepresentation.PyTypeRepresentationParserDefinition +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyFunctionTypeRepresentation +import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyTypeRepresentationFile +import com.jetbrains.python.documentation.doctest.PyDocstringTokenSetContributor +import com.jetbrains.python.psi.PyEllipsisLiteralExpression +import com.jetbrains.python.psi.PyExpression +import com.jetbrains.python.psi.PythonVisitorFilter +import com.jetbrains.python.psi.impl.PythonASTFactory +import junit.framework.TestCase +import java.io.IOException +import org.junit.jupiter.api.assertInstanceOf as assertInstanceOfJunit5 + +class PyTypeRepresentationParsingTest : ParsingTestCase("typeRepresentation/parsing", "pythonTypeRepresentation", + PyTypeRepresentationParserDefinition(), PythonParserDefinition()) { + @Throws(Exception::class) + override fun setUp() { + super.setUp() + application.registerService(PyElementTypesFacade::class.java, PyElementTypesFacadeImpl::class.java) + registerExtensionPoint(PythonDialectsTokenSetContributor.EP_NAME, + PythonDialectsTokenSetContributor::class.java) + registerExtension(PythonDialectsTokenSetContributor.EP_NAME, PythonTokenSetContributor()) + registerExtension(PythonDialectsTokenSetContributor.EP_NAME, PyDocstringTokenSetContributor()) + addExplicitExtension(LanguageASTFactory.INSTANCE, PythonLanguage.getInstance(), PythonASTFactory()) + } + + @Throws(Exception::class) + override fun tearDown() { + // clear cached extensions + try { + PythonVisitorFilter.INSTANCE.removeExplicitExtension(PythonLanguage.INSTANCE, + PythonVisitorFilter { _, _ -> false }) + PythonVisitorFilter.INSTANCE.removeExplicitExtension(PyTypeRepresentationDialect, + PythonVisitorFilter { _, _ -> false }) + } + catch (e: Throwable) { + addSuppressedException(e) + } + finally { + super.tearDown() + } + } + + override fun doCodeTest(code: String) { + try { + super.doCodeTest(code) + } + catch (e: IOException) { + throw AssertionError(e) + } + } + + fun parseCallable(code: String): PyFunctionTypeRepresentation { + doCodeTest(code) + return assertInstanceOfJunit5(parsedType) + } + + private val parsedType: PyExpression? get() = assertInstanceOfJunit5(myFile).type + + fun `test todo`() { + doCodeTest("@Todo(`Unpack[]` special form)") + } + + fun `test todo in callable`() { + val callable = parseCallable("(@Todo(`Unpack[]` special form)) -> None") + assertNotNull(callable) + assertSize(1, callable.parameterList.parameters) + } + + fun `test callable empty`() { + val callable = parseCallable("() -> None") + assertEmpty(callable.parameterList.parameters) + val returnType = callable.returnType + assertNotNull(returnType) + assertEquals("None", returnType!!.text) + } + + fun `test callable simple`() { + parseCallable("(int, str) -> None") + } + + fun `test callable named parameter`() { + parseCallable("(a: int) -> None") + } + + fun `test callable default parameter`() { + parseCallable("(a: int = ...) -> None") + } + + fun `test callable varargs`() { + parseCallable("(*int, **str) -> None") + } + + fun `test callable complex varargs`() { + parseCallable("(*a: *tuple[int, str], **b: **B) -> None") + } + + fun `test callable ellipsis`() { + val callable = parseCallable("(...) -> None") + val paramTypes = callable.parameterList.parameters + assertSize(1, paramTypes) + assertInstanceOfJunit5(paramTypes[0]) + val returnType = callable.returnType + assertNotNull(returnType) + } + + fun `test callable nested`() { + val callable = parseCallable("(int, (str) -> None, int) -> (str) -> None") + val paramTypes = callable.parameterList.parameters + assertSize(3, paramTypes) + val nestedCallable = assertInstanceOfJunit5(paramTypes[1]) + assertSize(1, nestedCallable.parameterList.parameters) + val nestedReturnType = nestedCallable.returnType + assertNotNull(nestedReturnType) + TestCase.assertEquals( + "None", nestedReturnType!!.text + ) + + val returnType = assertInstanceOfJunit5(paramTypes[1]) + assertSize(1, returnType.parameterList.parameters) + val returnTypeNestedReturnType = returnType.returnType + assertNotNull(returnTypeNestedReturnType) + TestCase.assertEquals( + "None", nestedReturnType.text + ) + } + + fun `test callable no return type`() { + val callable = parseCallable("(int) -> ") + val paramTypes = callable.parameterList.parameters + assertSize(1, paramTypes) + assertNull(callable.returnType) + } + + fun `test callable dangling comma`() { + parseCallable("(int,) -> None") + } + + fun `test callable empty function type`() { + doCodeTest("") + assertNull(parsedType) + } + + fun `test callable no type after star`() { + parseCallable("(*) -> int") + } + + override fun getTestDataPath(): String { + return PythonTestUtil.getTestDataPath() + } +}