/
SymbolSampleAnalysisEnvironment.kt
238 lines (206 loc) · 9.71 KB
/
SymbolSampleAnalysisEnvironment.kt
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
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package org.jetbrains.dokka.analysis.kotlin.symbols.services
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.impl.source.tree.LeafPsiElement
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin
import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironment
import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator
import org.jetbrains.dokka.analysis.kotlin.sample.SampleRewriter
import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet
import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLinkToSymbol
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SamplesKotlinAnalysis
import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.plugin
import org.jetbrains.dokka.plugability.query
import org.jetbrains.dokka.plugability.querySingle
import org.jetbrains.dokka.utilities.DokkaLogger
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbolOrigin
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.utils.addToStdlib.applyIf
internal class SymbolSampleAnalysisEnvironmentCreator(
private val context: DokkaContext,
) : SampleAnalysisEnvironmentCreator {
private val projectKotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis }
private val sampleRewriter by lazy {
val rewriters = context.plugin<KotlinAnalysisPlugin>().query { sampleRewriter }
if (rewriters.size > 1) context.logger.warn("There are more than one samples rewriters. Dokka does not support it.")
rewriters.singleOrNull()
}
override fun <T> use(block: SampleAnalysisEnvironment.() -> T): T {
return runBlocking(Dispatchers.Default) {
create().use(block)
}
}
override fun create(): SampleAnalysisEnvironment {
return SymbolSampleAnalysisEnvironment(
samplesKotlinAnalysis = SamplesKotlinAnalysis(
sourceSets = context.configuration.sourceSets,
context = context
),
projectKotlinAnalysis = projectKotlinAnalysis,
sampleRewriter = sampleRewriter,
dokkaLogger = context.logger
)
}
}
private class SymbolSampleAnalysisEnvironment(
private val samplesKotlinAnalysis: KotlinAnalysis,
private val projectKotlinAnalysis: KotlinAnalysis,
private val sampleRewriter: SampleRewriter?,
private val dokkaLogger: DokkaLogger,
) : SampleAnalysisEnvironment {
override fun resolveSample(
sourceSet: DokkaSourceSet,
fullyQualifiedLink: String
): SampleSnippet? {
val psiElement = findPsiElement(sourceSet, fullyQualifiedLink)
if (psiElement == null) {
dokkaLogger.warn(
"Unable to resolve a @sample link: \"$fullyQualifiedLink\". Is it used correctly? " +
"Expecting a link to a reachable (resolvable) top-level Kotlin function."
)
return null
} else if (psiElement.language != KotlinLanguage.INSTANCE) {
dokkaLogger.warn("Unable to resolve non-Kotlin @sample links: \"$fullyQualifiedLink\"")
return null
} else if (psiElement !is KtFunction) {
dokkaLogger.warn("Unable to process a @sample link: \"$fullyQualifiedLink\". Only function links allowed.")
return null
}
val imports = processImports(psiElement, sampleRewriter)
val body = processBody(psiElement)
return SampleSnippet(imports, body)
}
// TODO: remove after KT-53669 and use [org.jetbrains.kotlin.analysis.api.symbols.sourcePsiSafe] from Analysis API
private inline fun <reified PSI : PsiElement> KtSymbol.kotlinAndJavaSourcePsiSafe(): PSI? {
// TODO: support Java sources after KT-53669
val sourcePsi = when (origin) {
KtSymbolOrigin.SOURCE -> this.psi
KtSymbolOrigin.JAVA -> this.psi
KtSymbolOrigin.SOURCE_MEMBER_GENERATED -> null
KtSymbolOrigin.LIBRARY -> null
KtSymbolOrigin.SAM_CONSTRUCTOR -> null
KtSymbolOrigin.INTERSECTION_OVERRIDE -> null
KtSymbolOrigin.SUBSTITUTION_OVERRIDE -> null
KtSymbolOrigin.DELEGATED -> null
KtSymbolOrigin.JAVA_SYNTHETIC_PROPERTY -> null
KtSymbolOrigin.PROPERTY_BACKING_FIELD -> null
KtSymbolOrigin.PLUGIN -> null
KtSymbolOrigin.JS_DYNAMIC -> null
}
return sourcePsi as? PSI
}
private fun findPsiElement(sourceSet: DokkaSourceSet, fqLink: String): PsiElement? {
// fallback to default roots of the source set even if sample roots are assigned,
// because `@sample` tag can contain links to functions from project sources
return samplesKotlinAnalysis.findPsiElement(sourceSet, fqLink)
?: projectKotlinAnalysis.findPsiElement(sourceSet, fqLink)
}
private fun KotlinAnalysis.findPsiElement(sourceSet: DokkaSourceSet, fqLink: String): PsiElement? {
val ktSourceModule = this.getModuleOrNull(sourceSet) ?: return null
return analyze(ktSourceModule) {
resolveKDocTextLinkToSymbol(fqLink)
?.kotlinAndJavaSourcePsiSafe()
}
}
private fun processImports(psiElement: PsiElement, sampleRewriter: SampleRewriter?): List<String> {
val psiFile = psiElement.containingFile
val importsList = (psiFile as? KtFile)?.importList ?: return emptyList()
return importsList.imports
.map { it.text.removePrefix("import ") }
.filter { it.isNotBlank() }
.applyIf(sampleRewriter != null) {
mapNotNull { sampleRewriter?.rewriteImportDirective(it) }
}
}
private fun processBody(sampleElement: KtDeclarationWithBody): String {
return getSampleBody(sampleElement)
.trim { it == '\n' || it == '\r' }
.trimEnd()
.trimIndent()
}
private fun getSampleBody(psiElement: KtDeclarationWithBody): String {
val bodyExpression = psiElement.bodyExpression
val bodyExpressionText = bodyExpression!!.buildSampleText()
return when (bodyExpression) {
is KtBlockExpression -> bodyExpressionText.removeSurrounding("{", "}") // without braces according to the documentation of [SampleSnippet.body]
else -> bodyExpressionText
}
}
private fun PsiElement.buildSampleText(): String {
if (sampleRewriter == null) return this.text
val textBuilder = StringBuilder()
val errors = mutableListOf<SampleBuilder.ConvertError>()
this.accept(SampleBuilder(sampleRewriter, textBuilder, errors))
errors.forEach {
val st = it.e.stackTraceToString()
dokkaLogger.warn("Exception thrown while sample rewriting at ${containingFile.name}: (${it.loc})\n```\n${it.text}\n```\n$st")
}
return textBuilder.toString()
}
override fun close() {
samplesKotlinAnalysis.close()
}
}
private class SampleBuilder(
private val sampleRewriter: SampleRewriter,
val textBuilder: StringBuilder,
val errors: MutableList<ConvertError>
) : KtTreeVisitorVoid() {
data class ConvertError(val e: Exception, val text: String, val loc: String)
override fun visitCallExpression(expression: KtCallExpression) {
val callRewriter = expression.calleeExpression?.text?.let { sampleRewriter.getFunctionCallRewriter(it) }
if(callRewriter != null) {
val rewrittenResult = callRewriter.rewrite(
arguments = expression.valueArguments.map { it.text ?: "" }, // expect not nullable ASTDelegatePsiElement.text
typeArguments = expression.typeArguments.map { it.text ?: "" } // expect not nullable ASTDelegatePsiElement.text
)
textBuilder.append(rewrittenResult)
} else {
super.visitCallExpression(expression)
}
}
private fun reportProblemConvertingElement(element: PsiElement, e: Exception) {
val text = element.text
val document = PsiDocumentManager.getInstance(element.project).getDocument(element.containingFile)
val lineInfo = if (document != null) {
val lineNumber = document.getLineNumber(element.startOffset)
"$lineNumber, ${element.startOffset - document.getLineStartOffset(lineNumber)}"
} else {
"offset: ${element.startOffset}"
}
errors += ConvertError(e, text, lineInfo)
}
override fun visitElement(element: PsiElement) {
if (element is LeafPsiElement) {
textBuilder.append(element.text)
return
}
element.acceptChildren(object : PsiElementVisitor() {
override fun visitElement(element: PsiElement) {
try {
element.accept(this@SampleBuilder)
} catch (e: Exception) {
try {
reportProblemConvertingElement(element, e)
} finally {
textBuilder.append(element.text) //recover
}
}
}
})
}
}