@@ -6,6 +6,8 @@ import com.intellij.codeInsight.hints.Option
66import com.intellij.psi.PsiElement
77import com.intellij.psi.util.PsiTreeUtil
88import com.jetbrains.python.psi.*
9+ import com.jetbrains.python.psi.resolve.PyResolveContext
10+ import com.jetbrains.python.psi.types.PyCallableType
911import com.jetbrains.python.psi.types.TypeEvalContext
1012
1113
@@ -15,7 +17,6 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
1517 companion object {
1618 val classHints = Option (" hints.classes.parameters" , { " Class hints" }, true )
1719 val functionHints = Option (" hints.functions.parameters" , { " Function hints" }, true )
18- val lambdaHints = Option (" hints.lambdas.parameters" , { " Lambda hints" }, true )
1920 val hideOverlaps = Option (" hints.overlaps.parameters" , { " Hide overlaps" }, true )
2021 }
2122
@@ -27,14 +28,13 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
2728
2829 override fun getDescription () = " Help you pass correct arguments by showing parameter names at call sites"
2930
30- override fun getSupportedOptions () = listOf (classHints, functionHints, lambdaHints, hideOverlaps)
31+ override fun getSupportedOptions () = listOf (classHints, functionHints, hideOverlaps)
3132
3233 override fun getProperty (key : String? ): String? {
3334 val prefix = " inlay.parameters.hints"
3435 return when (key) {
3536 " $prefix .classes.parameters" -> " Show parameter names for class constructors and dataclasses."
3637 " $prefix .functions.parameters" -> " Show parameter names for function and method calls."
37- " $prefix .lambdas.parameters" -> " Show parameter names for lambda calls."
3838 " $prefix .overlaps.parameters" -> " Hide hints when a parameter name is completely overlapped by a longer argument name."
3939 else -> null
4040 }
@@ -44,102 +44,50 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
4444 val inlayInfos = mutableListOf<InlayInfo >()
4545
4646 // This method gets every element in the editor,
47- // so we have to verify it's a Python call expression
48- if (element !is PyCallExpression || element is PyDecorator ) {
49- return inlayInfos
50- }
47+ // so we have to verify it's a proper Python call expression
48+ if (element !is PyCallExpression || element is PyDecorator ) return inlayInfos
5149
5250 // Don't show hints if there's no arguments
5351 // Or the only argument is unpacking (*list, **dict)
5452 if (element.arguments.isEmpty() || (element.arguments.size == 1 && element.arguments[0 ] is PyStarArgument )) {
5553 return inlayInfos
5654 }
5755
58- // Try to resolve the object that made this call
59- var resolved = element.callee?.reference?.resolve() ? : return inlayInfos
60- if (isForbiddenBuiltinElement(resolved)) {
61- return inlayInfos
62- }
56+ // Implement settings based on the initial callee object
57+ val rootCalleeObject = element.callee?.reference?.resolve()
58+ if (rootCalleeObject is PyClass && ! classHints.isEnabled()) return inlayInfos
59+ if (rootCalleeObject !is PyClass && ! functionHints.isEnabled()) return inlayInfos
6360
64- var useCallMethod = false
65- if (resolved is PyTargetExpression ) {
66- // The target expression might include a lambda or class attribute
67- val assignedValue = resolved.findAssignedValue() ? : return inlayInfos
68- resolved = if (assignedValue is PyLambdaExpression && lambdaHints.isEnabled()) {
69- assignedValue
70- } else if (assignedValue is PyCallExpression ) {
71- // Potentially a class instance, very specific and requires more research
72- useCallMethod = true
73- assignedValue.callee?.reference?.resolve() ? : return inlayInfos
74- } else {
75- return inlayInfos
76- }
77- }
78-
79- var classAttributes = listOf<PyTargetExpression >()
80- if (resolved is PyClass && classHints.isEnabled()) {
81- // This call is made by a class (instantiation/__call__), so we want to find the parameters it takes.
82- // In order to do so, we first have to check for an init method, and if not found,
83- // We will use the class attributes instead. (Handle dataclasses, attrs, etc.)
84- val evalContext = TypeEvalContext .codeAnalysis(element.project, element.containingFile)
85- val entryMethod = if (useCallMethod) {
86- // TODO: Find some API sugar to make it more reliable?
87- resolved.findMethodByName(" __call__" , false , evalContext)
88- ? : resolved.findInitOrNew(true , evalContext)
89- } else {
90- resolved.findInitOrNew(true , evalContext)
91- }
92-
93- resolved = if (entryMethod != null && entryMethod.containingClass == resolved) {
94- entryMethod
95- } else {
96- // Use the class attributes if there's no init with params in the parent classes
97- // TODO: Make sure wrong attributes are not used
98- classAttributes = resolved.classAttributes
99- entryMethod ? : resolved
100- }
101- } else if (! functionHints.isEnabled()) {
102- return inlayInfos
103- }
61+ // Try to resolve the object that made this call, it can be a dataclass/method/function
62+ val evalContext = TypeEvalContext .codeCompletion(element.project, element.containingFile)
63+ val resolvedCallee = element.multiResolveCallee(PyResolveContext .defaultContext(evalContext))
64+ if (resolvedCallee.isEmpty() || isForbiddenBuiltinCallable(resolvedCallee[0 ])) return inlayInfos
10465
105- val resolvedParameters = getElementFilteredParameters(resolved)
106- val finalParameters = if (resolvedParameters.isEmpty() && classAttributes.isNotEmpty()) {
107- // If there's no parameters in the object,
108- // we use the class attributes instead,
109- // in case this is a class
110- classAttributes
111- } else if (resolvedParameters.isEmpty()) {
112- return inlayInfos
113- } else {
114- resolvedParameters
115- }
66+ // Get the parameters of the call, except `self`, `*` and `/`
67+ val resolvedParameters = resolvedCallee[0 ].getParameters(evalContext)?.filter { it ->
68+ ! it.isSelf && it.parameter !is PySingleStarParameter && it.parameter !is PySlashParameter
69+ } ? : return inlayInfos
11670
117- if (finalParameters.size == 1 ) {
118- // Don't need a hint if there's only one parameter,
119- // Make an exception for *args
120- finalParameters[0 ].let {
121- if (it !is PyNamedParameter || ! it.isPositionalContainer) return inlayInfos
122- }
123- }
71+ // Don't need a hint if there's only one parameter,
72+ // Make an exception for *args
73+ if (resolvedParameters.size == 1 && ! resolvedParameters[0 ].isPositionalContainer) return inlayInfos
12474
125- finalParameters .zip(element.arguments).forEach { (param, arg) ->
75+ resolvedParameters .zip(element.arguments).forEach { (param, arg) ->
12676 val paramName = param.name ? : return @forEach
12777 if (arg is PyStarArgument || arg is PyKeywordArgument ) {
12878 // It's a keyword argument or unpacking,
12979 // we don't need to show hits after this
13080 return inlayInfos
13181 }
13282
133- if (param is PyNamedParameter ) {
134- if (param.isPositionalContainer) {
135- // This is an *args parameter that takes more than one argument
136- // So we show it and stop the further processing of this call expression
137- inlayInfos.add(InlayInfo (" ...$paramName " , arg.textOffset))
138- return inlayInfos
139- } else if (param.isKeywordContainer) {
140- // We don't want to show `kwargs` as a hint by accident
141- return inlayInfos
142- }
83+ if (param.isPositionalContainer) {
84+ // This is an *args parameter that takes more than one argument
85+ // So we show it and stop the further processing of this call expression
86+ inlayInfos.add(InlayInfo (" ...$paramName " , arg.textOffset))
87+ return inlayInfos
88+ } else if (param.isKeywordContainer) {
89+ // We don't want to show `kwargs` as a hint by accident
90+ return inlayInfos
14391 }
14492
14593 if (isHintNameValid(paramName.lowercase(), arg)) {
@@ -171,26 +119,12 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
171119 }
172120
173121 /* *
174- * Get the parameters of the element, but filter out the ones that are not needed.
175- * For example, if the element is a class method, we don't want to show the __self__ parameter.
176- */
177- private fun getElementFilteredParameters (element : PsiElement ): List <PyParameter > {
178- element.children.forEach {
179- if (it is PyParameterList ) {
180- return it.parameters.filter { param ->
181- ! param.isSelf && param !is PySingleStarParameter && param !is PySlashParameter
182- }
183- }
184- }
185- return emptyList()
186- }
187-
188- /* *
189- * Checks if the element is part of the standard library that isn't relevant for these hints.
122+ * Checks if the callable is part of the standard library that isn't relevant for these hints.
190123 */
191- private fun isForbiddenBuiltinElement ( element : PsiElement ): Boolean {
124+ private fun isForbiddenBuiltinCallable ( callableType : PyCallableType ): Boolean {
192125 // TODO: Implement using PyType.isBuiltin (?),
193126 // although we still want some builtins like datetime.datetime
194- return element.containingFile.name in forbiddenBuiltinFiles
127+ val fileName = callableType.callable?.containingFile?.name ? : return false
128+ return fileName in forbiddenBuiltinFiles
195129 }
196130}
0 commit comments