summaryrefslogtreecommitdiff
path: root/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/parameterInfo/HintsTypeRenderer.kt
blob: 5f322ab21b7363327e45864559a1ee3a1340c91d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.kotlin.idea.parameterInfo

import org.jetbrains.kotlin.builtins.*
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotated
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.codeInsight.hints.InlayInfoDetail
import org.jetbrains.kotlin.idea.codeInsight.hints.TextInlayInfoDetail
import org.jetbrains.kotlin.idea.codeInsight.hints.TypeInlayInfoDetail
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.renderer.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.isCompanionObject
import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
import org.jetbrains.kotlin.types.*
import org.jetbrains.kotlin.types.error.*
import org.jetbrains.kotlin.types.typeUtil.isUnresolvedType
import org.jetbrains.kotlin.types.error.ErrorUtils
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import java.util.ArrayList

/**
 * copy-pasted and inspired by [DescriptorRendererImpl]
 *
 * To render any kotlin type into a sequence of short and human-readable kotlin types like
 * - Int
 * - Int?
 * - List<String?>?
 *
 * For each type short name and fqName is provided (see [TypeInlayInfoDetail]).
 */
class HintsTypeRenderer private constructor(override val options: HintsDescriptorRendererOptions) : KotlinIdeDescriptorRenderer(options) {

    init {
        check(options.isLocked) { "options have not been locked yet to prevent mutability" }
        check(options.textFormat == RenderingFormat.PLAIN) { "only PLAIN text format is supported" }
        check(!options.verbose) { "verbose mode is not supported" }
        check(!options.renderTypeExpansions) { "Type expansion rendering is unsupported" }
    }

    private val renderer = COMPACT_WITH_SHORT_TYPES.withOptions {}

    @Suppress("SuspiciousCollectionReassignment")
    private val functionTypeAnnotationsRenderer: HintsTypeRenderer by lazy {
        HintsTypeRenderer.withOptions {
            excludedTypeAnnotationClasses += listOf(StandardNames.FqNames.extensionFunctionType)
        }
    }

    private fun renderName(name: Name): String = escape(name.render())

    /* TYPES RENDERING */
    fun renderTypeIntoInlayInfo(type: KotlinType): List<InlayInfoDetail> {
        val list = mutableListOf<InlayInfoDetail>()
        return options.typeNormalizer.invoke(type).renderNormalizedTypeTo(list)
    }

    private fun MutableList<InlayInfoDetail>.append(text: String): MutableList<InlayInfoDetail> = apply {
        add(TextInlayInfoDetail(text))
    }

    private fun MutableList<InlayInfoDetail>.append(text: String, descriptor: ClassifierDescriptor?): MutableList<InlayInfoDetail> {
        descriptor?.let {
            add(TypeInlayInfoDetail(text, it.fqNameSafe.asString()))
        } ?: run {
            append(text)
        }
        return this
    }

    private fun KotlinType.renderNormalizedTypeTo(list: MutableList<InlayInfoDetail>): MutableList<InlayInfoDetail> {
        this.unwrap().safeAs<AbbreviatedType>()?.let { abbreviated ->
            // TODO nullability is lost for abbreviated type?
            abbreviated.abbreviation.renderNormalizedTypeAsIsTo(list)
            if (options.renderUnabbreviatedType) {
                abbreviated.renderAbbreviatedTypeExpansionTo(list)
            }
            return list
        }

        this.renderNormalizedTypeAsIsTo(list)
        return list
    }

    private fun AbbreviatedType.renderAbbreviatedTypeExpansionTo(list: MutableList<InlayInfoDetail>) {
        list.append(" /* = ")
        this.expandedType.renderNormalizedTypeAsIsTo(list)
        list.append(" */")
    }

    private fun KotlinType.renderNormalizedTypeAsIsTo(list:  MutableList<InlayInfoDetail>) {
        if (this is WrappedType && !this.isComputed()) {
            list.append("<Not computed yet>")
            return
        }
        when (val unwrappedType = this.unwrap()) {
            // KTIJ-19098: platform type (e.g. `String!`) is rendered like a plain text `String!` w/o fqName link
            is FlexibleType -> list.append(unwrappedType.render(renderer, options))
            is SimpleType -> unwrappedType.renderSimpleTypeTo(list)
        }
    }

    private fun SimpleType.renderSimpleTypeTo(list: MutableList<InlayInfoDetail>) {
        if (this == TypeUtils.CANNOT_INFER_FUNCTION_PARAM_TYPE || TypeUtils.isDontCarePlaceholder(this)) {
            list.append("???")
            return
        }
        if (ErrorUtils.isUninferredTypeVariable(this)) {
            if (options.uninferredTypeParameterAsName) {
                list.append(renderError((this.constructor as ErrorTypeConstructor).getParam(0)))
            } else {
                list.append("???")
            }
            return
        }

        if (this.isError) {
            this.renderDefaultTypeTo(list)
            return
        }
        if (shouldRenderAsPrettyFunctionType(this)) {
            this.renderFunctionTypeTo(list)
        } else {
            this.renderDefaultTypeTo(list)
        }
    }

    private fun List<TypeProjection>.renderTypeArgumentsTo(list: MutableList<InlayInfoDetail>) {
        if (this.isNotEmpty()) {
            list.append(lt())
            this.appendTypeProjectionsTo(list)
            list.append(gt())
        }
    }

    private fun KotlinType.renderDefaultTypeTo(list: MutableList<InlayInfoDetail>) {
        renderAnnotationsTo(list)

        if (this.isError) {
            if (isUnresolvedType(this) && options.presentableUnresolvedTypes) {
                list.append(this.debugMessage)
            } else {
                if (this is ErrorType && !options.informativeErrorType) {
                    list.append(this.debugMessage)
                } else {
                    list.append(this.constructor.toString()) // Debug name of an error type is more informative
                }
            }
            this.arguments.renderTypeArgumentsTo(list)
        } else {
            this.renderTypeConstructorAndArgumentsTo(list)
        }

        if (this.isMarkedNullable) {
            list.append("?")
        }

        if (this.isDefinitelyNotNullType) {
            list.append("& Any")
        }
    }

    private fun KotlinType.renderTypeConstructorAndArgumentsTo(
        list: MutableList<InlayInfoDetail>,
        typeConstructor: TypeConstructor = this.constructor
    ) {
        val possiblyInnerType = this.buildPossiblyInnerType()
        if (possiblyInnerType == null) {
            typeConstructor.renderTypeConstructorTo(list)
            this.arguments.renderTypeArgumentsTo(list)
            return
        }

        possiblyInnerType.renderPossiblyInnerTypeTo(list)
    }

    private fun PossiblyInnerType.renderPossiblyInnerTypeTo(list: MutableList<InlayInfoDetail>) {
        this.outerType?.let {
            it.renderPossiblyInnerTypeTo(list)
            list.append(".")
            list.append(renderName(this.classifierDescriptor.name))
        } ?: this.classifierDescriptor.typeConstructor.renderTypeConstructorTo(list)

        this.arguments.renderTypeArgumentsTo(list)
    }

    fun TypeConstructor.renderTypeConstructorTo(list: MutableList<InlayInfoDetail>){
        val text = when (val cd = this.declarationDescriptor) {
            is TypeParameterDescriptor, is ClassDescriptor, is TypeAliasDescriptor -> renderClassifierName(cd)
            null -> this.toString()
            else -> error("Unexpected classifier: " + cd::class.java)
        }
        list.append(text, this.declarationDescriptor)
    }

    override fun renderClassifierName(klass: ClassifierDescriptor): String = if (ErrorUtils.isError(klass)) {
        klass.typeConstructor.toString()
    } else
        options.hintsClassifierNamePolicy.renderClassifier(klass, this)

    private fun KotlinType.renderFunctionTypeTo(list: MutableList<InlayInfoDetail>) {
        val type = this
        val lengthBefore = list.size
        // we need special renderer to skip @ExtensionFunctionType
        with(functionTypeAnnotationsRenderer) {
            type.renderAnnotationsTo(list)
        }
        val hasAnnotations = list.size != lengthBefore

        val isSuspend = this.isSuspendFunctionType
        val isNullable = this.isMarkedNullable
        val receiverType = this.getReceiverTypeFromFunctionType()

        val needParenthesis = isNullable || (hasAnnotations && receiverType != null)
        if (needParenthesis) {
            if (isSuspend) {
                list.add(lengthBefore, TextInlayInfoDetail("("))
            } else {
                if (hasAnnotations) {
                    if (list[list.lastIndex - 1].text != ")") {
                        // last annotation rendered without parenthesis - need to add them otherwise parsing will be incorrect
                        list.add(list.lastIndex, TextInlayInfoDetail("()"))
                    }
                }

                list.append("(")
            }
        }

        if (receiverType != null) {
            val surroundReceiver = shouldRenderAsPrettyFunctionType(receiverType) && !receiverType.isMarkedNullable ||
                    receiverType.hasModifiersOrAnnotations()
            if (surroundReceiver) {
                list.append("(")
            }
            receiverType.renderNormalizedTypeTo(list)
            if (surroundReceiver) {
                list.append(")")
            }
            list.append(".")
        }

        list.append("(")

        val parameterTypes = this.getValueParameterTypesFromFunctionType()
        for ((index, typeProjection) in parameterTypes.withIndex()) {
            if (index > 0) list.append(", ")

            if (options.parameterNamesInFunctionalTypes) {
                typeProjection.type.extractParameterNameFromFunctionTypeArgument()?.let { name ->
                    list.append(renderName(name))
                    list.append(": ")
                }
            }

            typeProjection.renderTypeProjectionTo(list)
        }

        list.append(") ").append(arrow()).append(" ")
        this.getReturnTypeFromFunctionType().renderNormalizedTypeTo(list)

        if (needParenthesis) list.append(")")

        if (isNullable) list.append("?")
    }

    fun TypeProjection.renderTypeProjectionTo(list: MutableList<InlayInfoDetail>) =
        listOf(this).appendTypeProjectionsTo(list)

    private fun List<TypeProjection>.appendTypeProjectionsTo(list: MutableList<InlayInfoDetail>) {
        val iterator = this.iterator()
        while (iterator.hasNext()) {
            val next = iterator.next()

            if (next.isStarProjection) {
                list.append("*")
            } else {
                val renderedType = renderTypeIntoInlayInfo(next.type)
                if (next.projectionKind != Variance.INVARIANT) {
                    list.append("${next.projectionKind} ")
                }
                list.addAll(renderedType)
            }

            if (iterator.hasNext()) {
                list.append(", ")
            }
        }
    }

    private fun Annotated.renderAnnotationsTo(list: MutableList<InlayInfoDetail>, target: AnnotationUseSiteTarget? = null) {
        if (DescriptorRendererModifier.ANNOTATIONS !in options.modifiers) return

        val excluded = if (this is KotlinType) options.excludedTypeAnnotationClasses else options.excludedAnnotationClasses

        val annotationFilter = options.annotationFilter
        for (annotation in this.annotations) {
            if (annotation.fqName !in excluded
                && !annotation.isParameterName()
                && (annotationFilter == null || annotationFilter(annotation))
            ) {
                list.append(renderAnnotation(annotation, target))
                if (options.eachAnnotationOnNewLine) {
                    list.append("\n")
                } else {
                    list.append(" ")
                }
            }
        }
    }

    override fun renderAnnotation(annotation: AnnotationDescriptor, target: AnnotationUseSiteTarget?): String {
        return buildString {
            append('@')
            if (target != null) {
                append(target.renderName + ":")
            }
            val annotationType = annotation.type
            append(renderTypeIntoInlayInfo(annotationType))
        }
    }

    companion object {
        @JvmStatic
        private fun withOptions(changeOptions: HintsDescriptorRendererOptions.() -> Unit): HintsTypeRenderer {
            val options = HintsDescriptorRendererOptions()
            options.changeOptions()
            options.lock()
            return HintsTypeRenderer(options)
        }

        internal fun getInlayHintsTypeRenderer(bindingContext: BindingContext, context: KtElement) =
            withOptions {
                modifiers = emptySet()
                parameterNameRenderingPolicy = ParameterNameRenderingPolicy.ONLY_NON_SYNTHESIZED
                enhancedTypes = true
                textFormat = RenderingFormat.PLAIN
                renderUnabbreviatedType = false
                hintsClassifierNamePolicy = ImportAwareClassifierNamePolicy(bindingContext, context)
            }
    }

    internal class ImportAwareClassifierNamePolicy(
        val bindingContext: BindingContext,
        val context: KtElement
    ): HintsClassifierNamePolicy {
        override fun renderClassifier(classifier: ClassifierDescriptor, renderer: HintsTypeRenderer): String {
            if (classifier.containingDeclaration is ClassDescriptor) {
                val resolutionFacade = context.getResolutionFacade()
                val scope = context.getResolutionScope(bindingContext, resolutionFacade)
                if (scope.findClassifier(classifier.name, NoLookupLocation.FROM_IDE) == classifier) {
                    return classifier.name.asString()
                }
            }

            return shortNameWithCompanionNameSkip(classifier, renderer)
        }

        private fun shortNameWithCompanionNameSkip(classifier: ClassifierDescriptor, renderer: HintsTypeRenderer): String {
            if (classifier is TypeParameterDescriptor) return renderer.renderName(classifier.name)

            val qualifiedNameParts = classifier.parentsWithSelf
                .takeWhile { it is ClassifierDescriptor }
                .filter { !(it.isCompanionObject() && it.name == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT) }
                .mapTo(ArrayList()) { it.name }
                .reversed()

            return renderFqName(qualifiedNameParts)
        }

    }
}