blob: 33170f3502604dc4eb138e4b8fc3a4bf832da316 [file] [log] [blame]
package org.jetbrains.dokka
import com.google.inject.Guice
import com.google.inject.Injector
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiManager
import org.jetbrains.dokka.DokkaConfiguration.SourceRoot
import org.jetbrains.dokka.Utilities.DokkaAnalysisModule
import org.jetbrains.dokka.Utilities.DokkaOutputModule
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzer
import org.jetbrains.kotlin.resolve.TopDownAnalysisMode
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
import kotlin.system.measureTimeMillis
class DokkaGenerator(val logger: DokkaLogger,
val classpath: List<String>,
val sources: List<SourceRoot>,
val samples: List<String>,
val includes: List<String>,
val moduleName: String,
val options: DocumentationOptions) {
private val documentationModule = DocumentationModule(moduleName)
fun generate() {
val sourcesGroupedByPlatform = sources.groupBy { it.platforms.firstOrNull() }
for ((platform, roots) in sourcesGroupedByPlatform) {
appendSourceModule(platform, roots)
}
documentationModule.prepareForGeneration(options)
val timeBuild = measureTimeMillis {
logger.info("Generating pages... ")
val outputInjector = Guice.createInjector(DokkaOutputModule(options, logger))
outputInjector.getInstance(Generator::class.java).buildAll(documentationModule)
}
logger.info("done in ${timeBuild / 1000} secs")
}
private fun appendSourceModule(defaultPlatform: String?, sourceRoots: List<SourceRoot>) {
val sourcePaths = sourceRoots.map { it.path }
val environment = createAnalysisEnvironment(sourcePaths)
logger.info("Module: $moduleName")
logger.info("Output: ${File(options.outputDir)}")
logger.info("Sources: ${sourcePaths.joinToString()}")
logger.info("Classpath: ${environment.classpath.joinToString()}")
logger.info("Analysing sources and libraries... ")
val startAnalyse = System.currentTimeMillis()
val defaultPlatformAsList = defaultPlatform?.let { listOf(it) }.orEmpty()
val defaultPlatformsProvider = object : DefaultPlatformsProvider {
override fun getDefaultPlatforms(descriptor: DeclarationDescriptor): List<String> {
val containingFilePath = descriptor.sourcePsi()?.containingFile?.virtualFile?.canonicalPath
?.let { File(it).absolutePath }
val sourceRoot = containingFilePath?.let { path -> sourceRoots.find { path.startsWith(it.path) } }
return sourceRoot?.platforms ?: defaultPlatformAsList
}
}
val injector = Guice.createInjector(
DokkaAnalysisModule(environment, options, defaultPlatformsProvider, documentationModule.nodeRefGraph, logger))
buildDocumentationModule(injector, documentationModule, { isNotSample(it) }, includes)
val timeAnalyse = System.currentTimeMillis() - startAnalyse
logger.info("done in ${timeAnalyse / 1000} secs")
Disposer.dispose(environment)
}
fun createAnalysisEnvironment(sourcePaths: List<String>): AnalysisEnvironment {
val environment = AnalysisEnvironment(DokkaMessageCollector(logger))
environment.apply {
addClasspath(PathUtil.getJdkClassesRootsFromCurrentJre())
// addClasspath(PathUtil.getKotlinPathsForCompiler().getRuntimePath())
for (element in this@DokkaGenerator.classpath) {
addClasspath(File(element))
}
addSources(sourcePaths)
addSources(this@DokkaGenerator.samples)
loadLanguageVersionSettings(options.languageVersion, options.apiVersion)
}
return environment
}
fun isNotSample(file: PsiFile): Boolean {
val sourceFile = File(file.virtualFile!!.path)
return samples.none { sample ->
val canonicalSample = File(sample).canonicalPath
val canonicalSource = sourceFile.canonicalPath
canonicalSource.startsWith(canonicalSample)
}
}
}
class DokkaMessageCollector(val logger: DokkaLogger) : MessageCollector {
override fun clear() {
seenErrors = false
}
private var seenErrors = false
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation?) {
if (severity == CompilerMessageSeverity.ERROR) {
seenErrors = true
}
logger.error(MessageRenderer.PLAIN_FULL_PATHS.render(severity, message, location))
}
override fun hasErrors() = seenErrors
}
fun buildDocumentationModule(injector: Injector,
documentationModule: DocumentationModule,
filesToDocumentFilter: (PsiFile) -> Boolean = { file -> true },
includes: List<String> = listOf()) {
val coreEnvironment = injector.getInstance(KotlinCoreEnvironment::class.java)
val fragmentFiles = coreEnvironment.getSourceFiles().filter(filesToDocumentFilter)
val resolutionFacade = injector.getInstance(DokkaResolutionFacade::class.java)
val analyzer = resolutionFacade.getFrontendService(LazyTopDownAnalyzer::class.java)
analyzer.analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, fragmentFiles)
val fragments = fragmentFiles
.map { resolutionFacade.resolveSession.getPackageFragment(it.packageFqName) }
.filterNotNull()
.distinct()
val packageDocs = injector.getInstance(PackageDocs::class.java)
for (include in includes) {
packageDocs.parse(include, fragments)
}
if (documentationModule.content.isEmpty()) {
documentationModule.updateContent {
for (node in packageDocs.moduleContent.children) {
append(node)
}
}
}
with(injector.getInstance(DocumentationBuilder::class.java)) {
documentationModule.appendFragments(fragments, packageDocs.packageContent,
injector.getInstance(PackageDocumentationBuilder::class.java))
propagateExtensionFunctionsToSubclasses(fragments, resolutionFacade)
}
val javaFiles = coreEnvironment.getJavaSourceFiles().filter(filesToDocumentFilter)
with(injector.getInstance(JavaDocumentationBuilder::class.java)) {
javaFiles.map { appendFile(it, documentationModule, packageDocs.packageContent) }
}
}
fun KotlinCoreEnvironment.getJavaSourceFiles(): List<PsiJavaFile> {
val sourceRoots = configuration.get(JVMConfigurationKeys.CONTENT_ROOTS)
?.filterIsInstance<JavaSourceRoot>()
?.map { it.file }
?: listOf()
val result = arrayListOf<PsiJavaFile>()
val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file")
sourceRoots.forEach { sourceRoot ->
sourceRoot.absoluteFile.walkTopDown().forEach {
val vFile = localFileSystem.findFileByPath(it.path)
if (vFile != null) {
val psiFile = PsiManager.getInstance(project).findFile(vFile)
if (psiFile is PsiJavaFile) {
result.add(psiFile)
}
}
}
}
return result
}