mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-02-05 08:06:56 +07:00
PY-33261 Inlay parameter hints for Python method and function calls
Merge-request: IJ-MR-108471 Merged-by: Daniil Kalinin <Daniil.Kalinin@jetbrains.com> GitOrigin-RevId: 1d9451020a755c0aaf57c12b16829f8b31291e5d
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a61dc8fee2
commit
157b23ce53
@@ -257,6 +257,7 @@ runcfg.error.message.python.interpreter.is.invalid.configure=Please <a href="#">
|
||||
# Consoles messages
|
||||
python.console=Python Console
|
||||
python.console.history.root=Python Consoles
|
||||
python.console.run.anything.provider=Runs Python Console
|
||||
python.console.not.supported=Python console for {0} interpreter is not supported
|
||||
python.console.toolbar.action.available.non.interactive=The action is not available for non-interactive shell
|
||||
|
||||
@@ -1433,3 +1434,20 @@ black.sdk.not.configured.error=No project SDK configured for the project {0}
|
||||
black.sdk.not.configured.error.title=SDK not configured
|
||||
black.remote.sdk.exception.text=Black formatter invocation in Package mode is not allowed on remote SDKs
|
||||
black.sdk.selection.combobox.label=Select Python SDK:
|
||||
|
||||
inlay.parameters.python.show.class.constructor.call.parameter.names=Class constructor calls
|
||||
inlay.parameters.python.show.hints.for.non-literal.arguments=Non-literal arguments
|
||||
inlay.parameters.python.hints.blacklist.explanation=\
|
||||
<p>To disable hints for a method or a function, use one of the following patterns:</p><br>\
|
||||
<p style="margin-left: 5px">\
|
||||
<code><b>(*info)</b></code> - all single parameter functions where the parameter name ends with <em>info</em><br>\
|
||||
<code><b>(key, value)</b></code> - all functions with parameters <em>key</em> and <em>value</em><br>\
|
||||
<code><b>*.put(self, key, value)</b></code> - all <em>put</em> methods with <em>key</em> and <em>value</em> parameters<br>\
|
||||
<code><b>Clazz.foo(self, a, b)</b></code> - method <em>foo</em> of class <em>Clazz</em> with parameters <em>a</em> and <em>b</em><br>\
|
||||
<code><b>foo(\\<star\\>\\<star\\>kwargs)</b></code> - function <em>foo</em> with parameter <em>**kwargs</em><br>\
|
||||
</p><br>\
|
||||
<p>\
|
||||
Names or placeholders must be provided for all parameters, including the optional parameters.<br>\
|
||||
Qualified method names must include class names, or a placeholder for them.<br>\
|
||||
Use the "Do not show hints for current method" {0} action to add patterns when editing code.\
|
||||
</p>
|
||||
@@ -225,6 +225,9 @@
|
||||
<localInspection language="Python" shortName="PyPep8NamingInspection" suppressId="PyPep8Naming" bundle="messages.PyPsiBundle" key="INSP.NAME.pep8.naming" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyPep8NamingInspection"/>
|
||||
<localInspection language="Python" shortName="PyShadowingBuiltinsInspection" suppressId="PyShadowingBuiltins" bundle="messages.PyPsiBundle" key="INSP.NAME.shadowing.builtins" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyShadowingBuiltinsInspection"/>
|
||||
|
||||
<codeInsight.parameterNameHints language="Python"
|
||||
implementationClass="com.jetbrains.python.inlayHints.PythonInlayParameterHintsProvider"/>
|
||||
|
||||
<intentionAction>
|
||||
<className>com.jetbrains.python.codeInsight.intentions.PyConvertMethodToPropertyIntention</className>
|
||||
<bundleName>messages.PyPsiBundle</bundleName>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class Rectangle:
|
||||
def __init__(self, length, width):
|
||||
self.length = length
|
||||
self.width = width
|
||||
|
||||
Rectangle(10, 4)
|
||||
@@ -0,0 +1,7 @@
|
||||
def foo(a, b):
|
||||
print(a + b)
|
||||
|
||||
arg1 = 1
|
||||
arg2 = 2
|
||||
|
||||
foo(arg1, arg2)
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.inlayHints
|
||||
|
||||
import com.intellij.codeInsight.hints.HintInfo
|
||||
import com.intellij.codeInsight.hints.InlayInfo
|
||||
import com.intellij.codeInsight.hints.InlayParameterHintsProvider
|
||||
import com.intellij.codeInsight.hints.Option
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.keymap.KeymapUtil
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.util.QualifiedName
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.psi.*
|
||||
import com.jetbrains.python.psi.PyCallExpression.PyArgumentsMapping
|
||||
import com.jetbrains.python.psi.impl.PyBuiltinCache
|
||||
import com.jetbrains.python.psi.resolve.PyResolveContext
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
|
||||
class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
|
||||
|
||||
companion object {
|
||||
val showForClassConstructorCalls: Option = Option("python.show.class.constructor.call.parameter.names",
|
||||
PyBundle.messagePointer(
|
||||
"inlay.parameters.python.show.class.constructor.call.parameter.names"),
|
||||
true)
|
||||
|
||||
val showForNonLiteralArguments: Option = Option("python.show.hints.for.non-literal.arguments",
|
||||
PyBundle.messagePointer(
|
||||
"inlay.parameters.python.show.hints.for.non-literal.arguments"),
|
||||
true)
|
||||
}
|
||||
|
||||
private fun getInlayInfoForArgumentList(node: PyArgumentList): List<InlayInfo> {
|
||||
if (node.parent is PyClass || node.arguments.size == 1) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val context = TypeEvalContext.codeAnalysis(node.project, node.containingFile)
|
||||
val resolveContext = PyResolveContext.defaultContext(context)
|
||||
|
||||
val callExpression = node.callExpression ?: return emptyList()
|
||||
val argumentMappings = callExpression.multiMapArguments(resolveContext)
|
||||
val mapping = if (argumentMappings.isParameterHintSafeOverloads()) argumentMappings.first() else return emptyList()
|
||||
|
||||
val callable = mapping.callableType?.callable
|
||||
|
||||
if (callable == null || (PyUtil.isInitOrNewMethod(callable) && !showForClassConstructorCalls.isEnabled())) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val info = mutableListOf<InlayInfo>()
|
||||
val mappedParameters = mapping.mappedParameters
|
||||
|
||||
mappedParameters.forEach { (argument, parameter) ->
|
||||
if (parameter != null && argument != null) {
|
||||
if (parameter.isPositionalContainer) {
|
||||
info.add(InlayInfo("*${parameter.name}", argument.textOffset))
|
||||
return info
|
||||
}
|
||||
if (parameter.isKeywordContainer) {
|
||||
return info
|
||||
}
|
||||
if (argument !is PyKeywordArgument) {
|
||||
if (argument is PyLiteralExpression || showForNonLiteralArguments.isEnabled()) {
|
||||
info.add(InlayInfo("${parameter.name}", argument.textOffset))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether it is safe to show hints for a method.
|
||||
*
|
||||
* @return {@code true} if the method does not have overloads or all the overloads have the same signature, i.e.,
|
||||
* all parameters have the same names and are placed in the same order.
|
||||
* Otherwise, return {@code false}.
|
||||
*/
|
||||
private fun List<PyArgumentsMapping>.isParameterHintSafeOverloads(): Boolean {
|
||||
if (this.size == 1) return true
|
||||
|
||||
return this
|
||||
.asSequence()
|
||||
.map { mapping -> mapping.mappedParameters }
|
||||
.map { parameters -> parameters.values }
|
||||
.map { listOfParameters ->
|
||||
listOfParameters.map { parameter ->
|
||||
parameter.getPresentableText(false)
|
||||
}
|
||||
}
|
||||
.distinct()
|
||||
.count() == 1
|
||||
}
|
||||
|
||||
override fun getHintInfo(element: PsiElement): HintInfo? {
|
||||
if (element is PyArgumentList) {
|
||||
val parent = element.parent
|
||||
|
||||
if (parent is PyCallExpression) {
|
||||
val callee = parent.callee
|
||||
|
||||
if (callee == null) return null
|
||||
|
||||
val context = TypeEvalContext.codeAnalysis(element.project, element.containingFile)
|
||||
val resolveContext = PyResolveContext.defaultContext(context)
|
||||
|
||||
val callableType = parent.multiResolveCallee(resolveContext).firstOrNull() ?: return null
|
||||
val callable = callableType.callable
|
||||
|
||||
if (callable is PyQualifiedNameOwner) {
|
||||
val qName = callable.qualifiedName
|
||||
val fullQName = if (PyBuiltinCache.isInBuiltins(callee)) qName?.toQNameForBuiltins() else qName
|
||||
val parameterList = callable.parameterList
|
||||
|
||||
if (fullQName != null) {
|
||||
return HintInfo.MethodInfo(fullQName, parameterList.parameters
|
||||
.map { parameter ->
|
||||
parameter.asNamed?.getRepr(false)?.replace("*", "<star>")
|
||||
?: if (parameter is PySlashParameter) "/" else "*"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun String.toQNameForBuiltins(): String {
|
||||
val components = mutableListOf("builtins")
|
||||
components.addAll(this.split("."))
|
||||
return QualifiedName.fromComponents(components).toString()
|
||||
}
|
||||
|
||||
override fun getParameterHints(element: PsiElement, file: PsiFile) =
|
||||
if (element is PyArgumentList) {
|
||||
getInlayInfoForArgumentList(element)
|
||||
}
|
||||
else emptyList()
|
||||
|
||||
override fun getDefaultBlackList() =
|
||||
setOf("builtins.*",
|
||||
"typing.*")
|
||||
|
||||
override fun getBlacklistExplanationHTML(): String {
|
||||
return PyBundle.message("inlay.parameters.python.hints.blacklist.explanation",
|
||||
KeymapUtil.getFirstKeyboardShortcutText(IdeActions.ACTION_SHOW_INTENTION_ACTIONS))
|
||||
}
|
||||
|
||||
override fun getSupportedOptions() =
|
||||
listOf(showForClassConstructorCalls,
|
||||
showForNonLiteralArguments)
|
||||
|
||||
override fun isBlackListSupported(): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.inlayHints
|
||||
|
||||
import com.intellij.codeInsight.hints.Option
|
||||
import com.jetbrains.python.PythonFileType
|
||||
import com.jetbrains.python.fixtures.PyTestCase
|
||||
|
||||
class PyInlayParameterHintsTest : PyTestCase() {
|
||||
|
||||
fun testHintsShownForFunctionCallWithMultipleArguments() {
|
||||
doTest("""
|
||||
def foo(a, b, c):
|
||||
pass
|
||||
|
||||
foo(<hint text="a:"/>1, <hint text="b:"/>2, <hint text="c:"/>3)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testHintsNotShownForFunctionCallWithSingleArgument() {
|
||||
doTest("""
|
||||
def foo(a):
|
||||
pass
|
||||
|
||||
foo(1)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testHintsNotShownForKeywordArguments() {
|
||||
doTest("""
|
||||
def foo(a, b):
|
||||
pass
|
||||
|
||||
foo(<hint text="a:"/>1, b=2)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testHintsShownForClassConstructorCall() {
|
||||
doTestWithOptions("""
|
||||
class Clazz:
|
||||
def __init__(self, a, b, c):
|
||||
pass
|
||||
|
||||
c = Clazz(<hint text="a:"/>1, <hint text="b:"/>2, <hint text="c:"/>3)
|
||||
""".trimIndent(),
|
||||
PythonInlayParameterHintsProvider.showForClassConstructorCalls)
|
||||
}
|
||||
|
||||
fun testHintsShownOnlyForLiterals() {
|
||||
doTestWithOptions("""
|
||||
def foo(a, b):
|
||||
pass
|
||||
|
||||
x = "variable"
|
||||
foo(<hint text="a:"/>"literal", x)
|
||||
""".trimIndent(),
|
||||
PythonInlayParameterHintsProvider.showForNonLiteralArguments,
|
||||
enabled = false)
|
||||
}
|
||||
|
||||
fun testHintsForNonKeywordArguments() {
|
||||
doTest("""
|
||||
def foo(*args):
|
||||
pass
|
||||
|
||||
foo(<hint text="*args:"/>1, 2, 3, 4, 5)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testHintsForBuiltinFunctionCallsNotShownByDefault() {
|
||||
doTest("""
|
||||
class Clazz:
|
||||
pass
|
||||
|
||||
c = Clazz
|
||||
isinstance(c, Clazz)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testHintsForPositionalArgumentAndKwargs() {
|
||||
doTest("""
|
||||
def fun(arg, **kwargs):
|
||||
pass
|
||||
|
||||
fun(<hint text="arg:"/>1, a=2, b=3, c=4)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testsHintsNotShownForOverloads() {
|
||||
doTest("""
|
||||
from typing import overload, Any
|
||||
|
||||
@overload
|
||||
def bar(a: int, b: int) -> None:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def bar(c: str, d: str) -> None:
|
||||
pass
|
||||
|
||||
def bar(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
bar(1, 2)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testsHintsShownForOverloadsWithSameSignatures() {
|
||||
doTest("""
|
||||
from typing import overload, Any
|
||||
|
||||
|
||||
@overload
|
||||
def bar(a: int, b: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@overload
|
||||
def bar(a: str, b: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
bar(<hint text="a:"/>1, <hint text="b:"/>2)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
fun testsHintsNotShownForOverloadsWithDifferentSignatures() {
|
||||
doTest("""
|
||||
from typing import overload, Any
|
||||
|
||||
|
||||
@overload
|
||||
def bar(c: int, d: int, e: float) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@overload
|
||||
def bar(a: str, b: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def bar(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
bar(1, 2)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
private fun doTest(text: String) {
|
||||
enableAllHints()
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text)
|
||||
myFixture.testInlays()
|
||||
}
|
||||
|
||||
private fun doTestWithOptions(text: String, vararg options: Option, enabled: Boolean = true) {
|
||||
options.forEach { option-> option.set(enabled) }
|
||||
myFixture.configureByText(PythonFileType.INSTANCE, text)
|
||||
myFixture.testInlays()
|
||||
}
|
||||
|
||||
private fun enableAllHints() {
|
||||
PythonInlayParameterHintsProvider().supportedOptions.forEach { option: Option ->
|
||||
option.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user