Skip to content

Commit 426ee68

Browse files
committed
Rewrite parameter hints
1 parent b8a7f44 commit 426ee68

File tree

3 files changed

+36
-102
lines changed

3 files changed

+36
-102
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
- Type hints now support `async` functions [[#15](https://github.com/WhiteMemory99/Intellij-Python-Inlay-Params/pull/15)]
66
- Made type hints clickable, `Ctrl+LMB` to open the object reference [[#17](https://github.com/WhiteMemory99/Intellij-Python-Inlay-Params/pull/17)]
77

8+
### Changed
9+
- Parameter hints rewritten - now more reliable, with complete syntax coverage, including chained calls support.
10+
811
### Fixed
912
- Redundant parameter hints for names that start with "`__`" or 1 character long
1013
- Display of unnecessary type hints [[#16](https://github.com/WhiteMemory99/Intellij-Python-Inlay-Params/pull/16)]

src/main/kotlin/space/whitememory/pythoninlayparams/PythonInlayParameterHintsProvider.kt

+33-99
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.intellij.codeInsight.hints.Option
66
import com.intellij.psi.PsiElement
77
import com.intellij.psi.util.PsiTreeUtil
88
import com.jetbrains.python.psi.*
9+
import com.jetbrains.python.psi.resolve.PyResolveContext
10+
import com.jetbrains.python.psi.types.PyCallableType
911
import 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
}

src/main/resources/inlayProviders/Parameters/hints.lambdas.parameters.py

-3
This file was deleted.

0 commit comments

Comments
 (0)