@@ -6,6 +6,8 @@ import com.intellij.codeInsight.hints.Option
6
6
import com.intellij.psi.PsiElement
7
7
import com.intellij.psi.util.PsiTreeUtil
8
8
import com.jetbrains.python.psi.*
9
+ import com.jetbrains.python.psi.resolve.PyResolveContext
10
+ import com.jetbrains.python.psi.types.PyCallableType
9
11
import com.jetbrains.python.psi.types.TypeEvalContext
10
12
11
13
@@ -15,7 +17,6 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
15
17
companion object {
16
18
val classHints = Option (" hints.classes.parameters" , { " Class hints" }, true )
17
19
val functionHints = Option (" hints.functions.parameters" , { " Function hints" }, true )
18
- val lambdaHints = Option (" hints.lambdas.parameters" , { " Lambda hints" }, true )
19
20
val hideOverlaps = Option (" hints.overlaps.parameters" , { " Hide overlaps" }, true )
20
21
}
21
22
@@ -27,14 +28,13 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
27
28
28
29
override fun getDescription () = " Help you pass correct arguments by showing parameter names at call sites"
29
30
30
- override fun getSupportedOptions () = listOf (classHints, functionHints, lambdaHints, hideOverlaps)
31
+ override fun getSupportedOptions () = listOf (classHints, functionHints, hideOverlaps)
31
32
32
33
override fun getProperty (key : String? ): String? {
33
34
val prefix = " inlay.parameters.hints"
34
35
return when (key) {
35
36
" $prefix .classes.parameters" -> " Show parameter names for class constructors and dataclasses."
36
37
" $prefix .functions.parameters" -> " Show parameter names for function and method calls."
37
- " $prefix .lambdas.parameters" -> " Show parameter names for lambda calls."
38
38
" $prefix .overlaps.parameters" -> " Hide hints when a parameter name is completely overlapped by a longer argument name."
39
39
else -> null
40
40
}
@@ -44,102 +44,50 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
44
44
val inlayInfos = mutableListOf<InlayInfo >()
45
45
46
46
// 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
51
49
52
50
// Don't show hints if there's no arguments
53
51
// Or the only argument is unpacking (*list, **dict)
54
52
if (element.arguments.isEmpty() || (element.arguments.size == 1 && element.arguments[0 ] is PyStarArgument )) {
55
53
return inlayInfos
56
54
}
57
55
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
63
60
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
104
65
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
116
70
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
124
74
125
- finalParameters .zip(element.arguments).forEach { (param, arg) ->
75
+ resolvedParameters .zip(element.arguments).forEach { (param, arg) ->
126
76
val paramName = param.name ? : return @forEach
127
77
if (arg is PyStarArgument || arg is PyKeywordArgument ) {
128
78
// It's a keyword argument or unpacking,
129
79
// we don't need to show hits after this
130
80
return inlayInfos
131
81
}
132
82
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
143
91
}
144
92
145
93
if (isHintNameValid(paramName.lowercase(), arg)) {
@@ -171,26 +119,12 @@ class PythonInlayParameterHintsProvider : InlayParameterHintsProvider {
171
119
}
172
120
173
121
/* *
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.
190
123
*/
191
- private fun isForbiddenBuiltinElement ( element : PsiElement ): Boolean {
124
+ private fun isForbiddenBuiltinCallable ( callableType : PyCallableType ): Boolean {
192
125
// TODO: Implement using PyType.isBuiltin (?),
193
126
// 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
195
129
}
196
130
}
0 commit comments