diff options
Diffstat (limited to 'dokka-runners/dokkatoo/modules/dokkatoo-plugin')
74 files changed, 7592 insertions, 0 deletions
diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/api/dokkatoo-plugin.api b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/api/dokkatoo-plugin.api new file mode 100644 index 00000000..d767d2ec --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/api/dokkatoo-plugin.api @@ -0,0 +1,397 @@ +public abstract class dev/adamko/dokkatoo/DokkatooBasePlugin : org/gradle/api/Plugin { + public static final field Companion Ldev/adamko/dokkatoo/DokkatooBasePlugin$Companion; + public static final field EXTENSION_NAME Ljava/lang/String; + public static final field TASK_GROUP Ljava/lang/String; + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public final class dev/adamko/dokkatoo/DokkatooBasePlugin$Companion { + public final fun getDependencyContainerNames ()Ldev/adamko/dokkatoo/DokkatooBasePlugin$DependencyContainerNames; + public final fun getTaskNames ()Ldev/adamko/dokkatoo/DokkatooBasePlugin$TaskNames; +} + +public final class dev/adamko/dokkatoo/DokkatooBasePlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun <init> (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract class dev/adamko/dokkatoo/DokkatooExtension : java/io/Serializable, org/gradle/api/plugins/ExtensionAware { + public abstract fun getDokkatooCacheDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getDokkatooConfigurationsDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getDokkatooModuleDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getDokkatooPublicationDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public final fun getDokkatooPublications ()Lorg/gradle/api/NamedDomainObjectContainer; + public final fun getDokkatooSourceSets ()Lorg/gradle/api/NamedDomainObjectContainer; + public abstract fun getModuleName ()Lorg/gradle/api/provider/Property; + public abstract fun getModulePath ()Lorg/gradle/api/provider/Property; + public abstract fun getModuleVersion ()Lorg/gradle/api/provider/Property; + public final fun getPluginsConfiguration ()Lorg/gradle/api/ExtensiblePolymorphicDomainObjectContainer; + public abstract fun getSourceSetScopeDefault ()Lorg/gradle/api/provider/Property; + public final fun getVersions ()Ldev/adamko/dokkatoo/DokkatooExtension$Versions; +} + +public abstract interface class dev/adamko/dokkatoo/DokkatooExtension$Versions : org/gradle/api/plugins/ExtensionAware { + public static final field Companion Ldev/adamko/dokkatoo/DokkatooExtension$Versions$Companion; + public abstract fun getFreemarker ()Lorg/gradle/api/provider/Property; + public abstract fun getJetbrainsDokka ()Lorg/gradle/api/provider/Property; + public abstract fun getJetbrainsMarkdown ()Lorg/gradle/api/provider/Property; + public abstract fun getKotlinxCoroutines ()Lorg/gradle/api/provider/Property; + public abstract fun getKotlinxHtml ()Lorg/gradle/api/provider/Property; +} + +public final class dev/adamko/dokkatoo/DokkatooExtension$Versions$Companion { +} + +public abstract class dev/adamko/dokkatoo/DokkatooPlugin : org/gradle/api/Plugin { + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public abstract class dev/adamko/dokkatoo/dokka/DokkaPublication : java/io/Serializable, org/gradle/api/Named, org/gradle/api/plugins/ExtensionAware { + public abstract fun getCacheRoot ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getDelayTemplateSubstitution ()Lorg/gradle/api/provider/Property; + public abstract fun getEnabled ()Lorg/gradle/api/provider/Property; + public abstract fun getFailOnWarning ()Lorg/gradle/api/provider/Property; + public abstract fun getFinalizeCoroutines ()Lorg/gradle/api/provider/Property; + public final fun getFormatName ()Ljava/lang/String; + public abstract fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getModuleName ()Lorg/gradle/api/provider/Property; + public abstract fun getModuleVersion ()Lorg/gradle/api/provider/Property; + public fun getName ()Ljava/lang/String; + public abstract fun getOfflineMode ()Lorg/gradle/api/provider/Property; + public abstract fun getOutputDir ()Lorg/gradle/api/file/DirectoryProperty; + public final fun getPluginsConfiguration ()Lorg/gradle/api/ExtensiblePolymorphicDomainObjectContainer; + public abstract fun getSuppressInheritedMembers ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppressObviousFunctions ()Lorg/gradle/api/provider/Property; +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaExternalDocumentationLinkSpec : java/io/Serializable, org/gradle/api/Named { + public abstract fun getEnabled ()Lorg/gradle/api/provider/Property; + public fun getName ()Ljava/lang/String; + public abstract fun getPackageListUrl ()Lorg/gradle/api/provider/Property; + public abstract fun getUrl ()Lorg/gradle/api/provider/Property; + public final fun packageListUrl (Ljava/lang/String;)V + public final fun packageListUrl (Lorg/gradle/api/provider/Provider;)V + public final fun url (Ljava/lang/String;)V + public final fun url (Lorg/gradle/api/provider/Provider;)V +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaGeneratorParametersSpec : org/gradle/api/plugins/ExtensionAware { + public abstract fun getDokkaModuleFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getDokkaSourceSets ()Lorg/gradle/api/NamedDomainObjectContainer; + public abstract fun getFailOnWarning ()Lorg/gradle/api/provider/Property; + public abstract fun getFinalizeCoroutines ()Lorg/gradle/api/provider/Property; + public abstract fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getModuleName ()Lorg/gradle/api/provider/Property; + public abstract fun getModuleVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getOfflineMode ()Lorg/gradle/api/provider/Property; + public abstract fun getPluginsClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getPluginsConfiguration ()Lorg/gradle/api/ExtensiblePolymorphicDomainObjectContainer; + public abstract fun getSuppressInheritedMembers ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppressObviousFunctions ()Lorg/gradle/api/provider/Property; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/DokkaModuleDescriptionKxs$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Ldev/adamko/dokkatoo/dokka/parameters/DokkaModuleDescriptionKxs$$serializer; + public fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/adamko/dokkatoo/dokka/parameters/DokkaModuleDescriptionKxs; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/adamko/dokkatoo/dokka/parameters/DokkaModuleDescriptionKxs;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/DokkaModuleDescriptionKxs$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaPackageOptionsSpec : dev/adamko/dokkatoo/dokka/parameters/HasConfigurableVisibilityModifiers, java/io/Serializable { + public abstract fun getDocumentedVisibilities ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getMatchingRegex ()Lorg/gradle/api/provider/Property; + public abstract fun getReportUndocumented ()Lorg/gradle/api/provider/Property; + public abstract fun getSkipDeprecated ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppress ()Lorg/gradle/api/provider/Property; +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceLinkSpec : java/io/Serializable { + public abstract fun getLocalDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getRemoteLineSuffix ()Lorg/gradle/api/provider/Property; + public abstract fun getRemoteUrl ()Lorg/gradle/api/provider/Property; + public final fun remoteUrl (Ljava/lang/String;)V + public final fun remoteUrl (Lorg/gradle/api/provider/Provider;)V +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetIdSpec : java/io/Serializable, org/gradle/api/Named { + public static final field Companion Ldev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetIdSpec$Companion; + public fun equals (Ljava/lang/Object;)Z + public fun getName ()Ljava/lang/String; + public final fun getScopeId ()Ljava/lang/String; + public final fun getSourceSetName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetIdSpec$Companion { +} + +public abstract class dev/adamko/dokkatoo/dokka/parameters/DokkaSourceSetSpec : dev/adamko/dokkatoo/dokka/parameters/HasConfigurableVisibilityModifiers, java/io/Serializable, org/gradle/api/Named, org/gradle/api/plugins/ExtensionAware { + public abstract fun getAnalysisPlatform ()Lorg/gradle/api/provider/Property; + public abstract fun getApiVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getDependentSourceSets ()Lorg/gradle/api/NamedDomainObjectContainer; + public abstract fun getDisplayName ()Lorg/gradle/api/provider/Property; + public abstract fun getDocumentedVisibilities ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getEnableAndroidDocumentationLink ()Lorg/gradle/api/provider/Property; + public abstract fun getEnableJdkDocumentationLink ()Lorg/gradle/api/provider/Property; + public abstract fun getEnableKotlinStdLibDocumentationLink ()Lorg/gradle/api/provider/Property; + public final fun getExternalDocumentationLinks ()Lorg/gradle/api/NamedDomainObjectContainer; + public abstract fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getJdkVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getLanguageVersion ()Lorg/gradle/api/provider/Property; + public fun getName ()Ljava/lang/String; + public abstract fun getPerPackageOptions ()Lorg/gradle/api/DomainObjectSet; + public abstract fun getReportUndocumented ()Lorg/gradle/api/provider/Property; + public abstract fun getSamples ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getSkipDeprecated ()Lorg/gradle/api/provider/Property; + public abstract fun getSkipEmptyPackages ()Lorg/gradle/api/provider/Property; + public abstract fun getSourceLinks ()Lorg/gradle/api/DomainObjectSet; + public abstract fun getSourceRoots ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getSourceSetId ()Lorg/gradle/api/provider/Provider; + public abstract fun getSourceSetScope ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppress ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppressGeneratedFiles ()Lorg/gradle/api/provider/Property; + public abstract fun getSuppressedFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun perPackageOption (Lorg/gradle/api/Action;)V + public final fun sourceLink (Lorg/gradle/api/Action;)V +} + +public final class dev/adamko/dokkatoo/dokka/parameters/KotlinPlatform : java/lang/Enum { + public static final field AndroidJVM Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static final field Common Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static final field Companion Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform$Companion; + public static final field JS Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static final field JVM Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static final field Native Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static final field WASM Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static fun valueOf (Ljava/lang/String;)Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public static fun values ()[Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/KotlinPlatform$Companion { + public final fun fromString (Ljava/lang/String;)Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; + public final fun getDEFAULT ()Ldev/adamko/dokkatoo/dokka/parameters/KotlinPlatform; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/VisibilityModifier : java/lang/Enum { + public static final field Companion Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier$Companion; + public static final field INTERNAL Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static final field PACKAGE Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static final field PRIVATE Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static final field PROTECTED Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static final field PUBLIC Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static fun valueOf (Ljava/lang/String;)Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; + public static fun values ()[Ldev/adamko/dokkatoo/dokka/parameters/VisibilityModifier; +} + +public final class dev/adamko/dokkatoo/dokka/parameters/VisibilityModifier$Companion { +} + +public abstract class dev/adamko/dokkatoo/dokka/plugins/DokkaHtmlPluginParameters : dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBaseSpec { + public static final field Companion Ldev/adamko/dokkatoo/dokka/plugins/DokkaHtmlPluginParameters$Companion; + public static final field DOKKA_HTML_PARAMETERS_NAME Ljava/lang/String; + public static final field DOKKA_HTML_PLUGIN_FQN Ljava/lang/String; + public abstract fun getCustomAssets ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getCustomStyleSheets ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getFooterMessage ()Lorg/gradle/api/provider/Property; + public abstract fun getMergeImplicitExpectActualDeclarations ()Lorg/gradle/api/provider/Property; + public abstract fun getSeparateInheritedMembers ()Lorg/gradle/api/provider/Property; + public abstract fun getTemplatesDir ()Lorg/gradle/api/file/DirectoryProperty; + public fun jsonEncode ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/DokkaHtmlPluginParameters$Companion { +} + +public abstract class dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBaseSpec : java/io/Serializable, org/gradle/api/Named { + public fun getName ()Ljava/lang/String; + public fun getPluginFqn ()Ljava/lang/String; + public abstract fun jsonEncode ()Ljava/lang/String; +} + +public abstract class dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder : dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBaseSpec { + public static final field Companion Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder$Companion; + public fun getPluginFqn ()Ljava/lang/String; + public fun jsonEncode ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder$Companion { +} + +public final class dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilderKt { + public static final fun PluginConfigBooleanValue (Lorg/gradle/api/provider/Provider;)Lorg/gradle/api/provider/Provider; + public static final fun PluginConfigNumberValue (Lorg/gradle/api/provider/Provider;)Lorg/gradle/api/provider/Provider; + public static final fun PluginConfigStringValue (Lorg/gradle/api/provider/Provider;)Lorg/gradle/api/provider/Provider; + public static final fun PluginConfigValue (Ljava/lang/Number;)Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$NumberValue; + public static final fun PluginConfigValue (Ljava/lang/String;)Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$StringValue; + public static final fun PluginConfigValue (Z)Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$BooleanValue; + public static final fun add (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Ljava/lang/Number;)V + public static final fun add (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Ljava/lang/String;)V + public static final fun add (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Z)V + public static final fun addBoolean (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Lorg/gradle/api/provider/Provider;)V + public static final fun addNumber (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Lorg/gradle/api/provider/Provider;)V + public static final fun addString (Ldev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values;Lorg/gradle/api/provider/Provider;)V + public static final fun booleanProperty (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Lorg/gradle/api/provider/Provider;)V + public static final fun files (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun numberProperty (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Lorg/gradle/api/provider/Provider;)V + public static final fun pluginParameters (Lorg/gradle/api/ExtensiblePolymorphicDomainObjectContainer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun properties (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun property (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Ljava/lang/Number;)V + public static final fun property (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Ljava/lang/String;)V + public static final fun property (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Z)V + public static final fun stringProperty (Ldev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBuilder;Ljava/lang/String;Lorg/gradle/api/provider/Provider;)V +} + +public abstract class dev/adamko/dokkatoo/dokka/plugins/DokkaVersioningPluginParameters : dev/adamko/dokkatoo/dokka/plugins/DokkaPluginParametersBaseSpec { + public static final field Companion Ldev/adamko/dokkatoo/dokka/plugins/DokkaVersioningPluginParameters$Companion; + public static final field DOKKA_VERSIONING_PLUGIN_FQN Ljava/lang/String; + public static final field DOKKA_VERSIONING_PLUGIN_PARAMETERS_NAME Ljava/lang/String; + public abstract fun getOlderVersions ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getOlderVersionsDir ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getRenderVersionsNavigationOnAllPages ()Lorg/gradle/api/provider/Property; + public abstract fun getVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getVersionsOrdering ()Lorg/gradle/api/provider/ListProperty; + public fun jsonEncode ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/DokkaVersioningPluginParameters$Companion { +} + +public abstract interface class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$BooleanValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Primitive { + public fun <init> (Z)V + public final fun getBoolean ()Z +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$DirectoryValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { + public fun <init> (Lorg/gradle/api/file/DirectoryProperty;)V + public final fun getDirectory ()Lorg/gradle/api/file/DirectoryProperty; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$FileValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { + public fun <init> (Lorg/gradle/api/file/RegularFileProperty;)V + public final fun getFile ()Lorg/gradle/api/file/RegularFileProperty; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$FilesValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { + public fun <init> (Lorg/gradle/api/file/ConfigurableFileCollection;)V + public final fun getFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$NumberValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Primitive { + public fun <init> (Ljava/lang/Number;)V + public final fun getNumber ()Ljava/lang/Number; +} + +public abstract interface class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Primitive : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Properties : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { + public fun <init> (Lorg/gradle/api/provider/MapProperty;)V + public final fun getValues ()Lorg/gradle/api/provider/MapProperty; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$StringValue : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Primitive { + public fun <init> (Ljava/lang/String;)V + public final fun getString ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue$Values : dev/adamko/dokkatoo/dokka/plugins/PluginConfigValue { + public fun <init> (Lorg/gradle/api/provider/ListProperty;)V + public final fun getValues ()Lorg/gradle/api/provider/ListProperty; +} + +public abstract class dev/adamko/dokkatoo/formats/DokkatooFormatPlugin : org/gradle/api/Plugin { + public fun <init> (Ljava/lang/String;)V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V + public fun configure (Ldev/adamko/dokkatoo/formats/DokkatooFormatPlugin$DokkatooFormatPluginContext;)V + public final fun getFormatName ()Ljava/lang/String; +} + +public final class dev/adamko/dokkatoo/formats/DokkatooFormatTasks$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun <init> (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract class dev/adamko/dokkatoo/formats/DokkatooGfmPlugin : dev/adamko/dokkatoo/formats/DokkatooFormatPlugin { + public fun configure (Ldev/adamko/dokkatoo/formats/DokkatooFormatPlugin$DokkatooFormatPluginContext;)V +} + +public abstract class dev/adamko/dokkatoo/formats/DokkatooHtmlPlugin : dev/adamko/dokkatoo/formats/DokkatooFormatPlugin { + public fun configure (Ldev/adamko/dokkatoo/formats/DokkatooFormatPlugin$DokkatooFormatPluginContext;)V +} + +public final class dev/adamko/dokkatoo/formats/DokkatooHtmlPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun <init> (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract class dev/adamko/dokkatoo/formats/DokkatooJavadocPlugin : dev/adamko/dokkatoo/formats/DokkatooFormatPlugin { + public fun configure (Ldev/adamko/dokkatoo/formats/DokkatooFormatPlugin$DokkatooFormatPluginContext;)V +} + +public abstract class dev/adamko/dokkatoo/formats/DokkatooJekyllPlugin : dev/adamko/dokkatoo/formats/DokkatooFormatPlugin { + public fun configure (Ldev/adamko/dokkatoo/formats/DokkatooFormatPlugin$DokkatooFormatPluginContext;)V +} + +public abstract interface annotation class dev/adamko/dokkatoo/internal/DokkatooInternalApi : java/lang/annotation/Annotation { +} + +public abstract class dev/adamko/dokkatoo/tasks/DokkatooGenerateTask : dev/adamko/dokkatoo/tasks/DokkatooTask { + public abstract fun getCacheDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getGenerationType ()Lorg/gradle/api/provider/Property; + public final fun getGenerator ()Ldev/adamko/dokkatoo/dokka/parameters/DokkaGeneratorParametersSpec; + public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getPublicationEnabled ()Lorg/gradle/api/provider/Property; + public abstract fun getRuntimeClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getWorkerDebugEnabled ()Lorg/gradle/api/provider/Property; + public abstract fun getWorkerJvmArgs ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getWorkerLogFile ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getWorkerMaxHeapSize ()Lorg/gradle/api/provider/Property; + public abstract fun getWorkerMinHeapSize ()Lorg/gradle/api/provider/Property; +} + +public final class dev/adamko/dokkatoo/tasks/DokkatooGenerateTask$GenerationType : java/lang/Enum { + public static final field MODULE Ldev/adamko/dokkatoo/tasks/DokkatooGenerateTask$GenerationType; + public static final field PUBLICATION Ldev/adamko/dokkatoo/tasks/DokkatooGenerateTask$GenerationType; + public static fun valueOf (Ljava/lang/String;)Ldev/adamko/dokkatoo/tasks/DokkatooGenerateTask$GenerationType; + public static fun values ()[Ldev/adamko/dokkatoo/tasks/DokkatooGenerateTask$GenerationType; +} + +public abstract class dev/adamko/dokkatoo/tasks/DokkatooPrepareModuleDescriptorTask : dev/adamko/dokkatoo/tasks/DokkatooTask { + public abstract fun getDokkaModuleDescriptorJson ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getModuleDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getModuleName ()Lorg/gradle/api/provider/Property; + public abstract fun getModulePath ()Lorg/gradle/api/provider/Property; +} + +public abstract class dev/adamko/dokkatoo/tasks/DokkatooTask : org/gradle/api/DefaultTask { + public abstract fun getObjects ()Lorg/gradle/api/model/ObjectFactory; +} + +public abstract class dev/adamko/dokkatoo/tasks/LogHtmlPublicationLinkTask : dev/adamko/dokkatoo/tasks/DokkatooTask { + public static final field Companion Ldev/adamko/dokkatoo/tasks/LogHtmlPublicationLinkTask$Companion; + public static final field ENABLE_TASK_PROPERTY_NAME Ljava/lang/String; + public final fun exec ()V + public abstract fun getIndexHtmlPath ()Lorg/gradle/api/provider/Property; + public abstract fun getServerUri ()Lorg/gradle/api/provider/Property; +} + +public final class dev/adamko/dokkatoo/tasks/LogHtmlPublicationLinkTask$Companion { +} + diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/build.gradle.kts b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/build.gradle.kts new file mode 100644 index 00000000..8bb60f57 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/build.gradle.kts @@ -0,0 +1,254 @@ +@file:Suppress("UnstableApiUsage") // jvm test suites & test report aggregation are incubating + +import buildsrc.utils.buildDir_ +import buildsrc.utils.skipTestFixturesPublications + +plugins { + buildsrc.conventions.`kotlin-gradle-plugin` + kotlin("plugin.serialization") + + dev.adamko.kotlin.`binary-compatibility-validator` + + dev.adamko.`dokkatoo-html` + buildsrc.conventions.`maven-publishing` + + `java-test-fixtures` + `jvm-test-suite` + `test-report-aggregation` + buildsrc.conventions.`maven-publish-test` +} + +description = "Generates documentation for Kotlin projects (using Dokka)" + +dependencies { + // ideally there should be a 'dokka-core-api' dependency (that is very thin and doesn't drag in loads of unnecessary code) + // that would be used as an implementation dependency, while dokka-core would be used as a compileOnly dependency + // https://github.com/Kotlin/dokka/issues/2933 + implementation(libs.kotlin.dokkaCore) + + compileOnly(libs.gradlePlugin.kotlin) + compileOnly(libs.gradlePlugin.kotlin.klibCommonizerApi) + compileOnly(libs.gradlePlugin.android) + compileOnly(libs.gradlePlugin.androidApi) + + implementation(platform(libs.kotlinxSerialization.bom)) + implementation(libs.kotlinxSerialization.json) + + testFixturesImplementation(gradleApi()) + testFixturesImplementation(gradleTestKit()) + + testFixturesCompileOnly(libs.kotlin.dokkaCore) + testFixturesImplementation(platform(libs.kotlinxSerialization.bom)) + testFixturesImplementation(libs.kotlinxSerialization.json) + + testFixturesCompileOnly(libs.kotlin.dokkaCore) + + testFixturesApi(platform(libs.kotest.bom)) + testFixturesApi(libs.kotest.junit5Runner) + testFixturesApi(libs.kotest.assertionsCore) + testFixturesApi(libs.kotest.assertionsJson) + testFixturesApi(libs.kotest.datatest) + + // don't define test dependencies here, instead define them in the testing.suites {} configuration below +} + +gradlePlugin { + isAutomatedPublishing = true + + plugins.register("dokkatoo") { + id = "org.jetbrains.dokka.dokkatoo" + displayName = "Dokkatoo" + description = "Generates documentation for Kotlin projects (using Dokka)" + implementationClass = "org.jetbrains.dokka.dokkatoo.DokkatooPlugin" + } + + fun registerDokkaPlugin( + pluginClass: String, + shortName: String, + longName: String = shortName, + ) { + plugins.register(pluginClass) { + id = "org.jetbrains.dokka.dokkatoo-${shortName.toLowerCase()}" + displayName = "Dokkatoo $shortName" + description = "Generates $longName documentation for Kotlin projects (using Dokka)" + implementationClass = "org.jetbrains.dokka.dokkatoo.formats.$pluginClass" + } + } + registerDokkaPlugin("DokkatooGfmPlugin", "GFM", longName = "GFM (GitHub Flavoured Markdown)") + registerDokkaPlugin("DokkatooHtmlPlugin", "HTML") + registerDokkaPlugin("DokkatooJavadocPlugin", "Javadoc") + registerDokkaPlugin("DokkatooJekyllPlugin", "Jekyll") + + plugins.configureEach { + website.set("https://github.com/adamko-dev/dokkatoo/") + vcsUrl.set("https://github.com/adamko-dev/dokkatoo.git") + tags.addAll( + "dokka", + "dokkatoo", + "kotlin", + "kdoc", + "android", + "documentation", + "javadoc", + "html", + "markdown", + "gfm", + "website", + ) + } +} + +kotlin { + target { + compilations.configureEach { + // TODO Dokkatoo uses Gradle 8, while Dokka uses Gradle 7, which has an older version of Kotlin that + // doesn't include these options - so update them or update Gradle. +// compilerOptions.configure { +// freeCompilerArgs.addAll( +// "-opt-in=org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi", +// ) +// } + } + } +} + +testing.suites { + withType<JvmTestSuite>().configureEach { + useJUnitJupiter() + + dependencies { + implementation(project.dependencies.gradleTestKit()) + + implementation(project.dependencies.testFixtures(project())) + + implementation(project.dependencies.platform(libs.kotlinxSerialization.bom)) + implementation(libs.kotlinxSerialization.json) + } + + targets.configureEach { + testTask.configure { + val projectTestTempDirPath = "$buildDir_/test-temp-dir" + inputs.property("projectTestTempDir", projectTestTempDirPath) + systemProperty("projectTestTempDir", projectTestTempDirPath) + + when (testType.get()) { + TestSuiteType.FUNCTIONAL_TEST, + TestSuiteType.INTEGRATION_TEST -> { + dependsOn(tasks.matching { it.name == "publishAllPublicationsToTestRepository" }) + + systemProperties( + "testMavenRepoDir" to file(mavenPublishTest.testMavenRepo).canonicalPath, + ) + + // depend on the test-publication task, but not the test-maven repo + // (otherwise this task will never be up-to-date) + dependsOn(tasks.publishToTestMavenRepo) + } + } + } + } + } + + + /** Unit tests suite */ + val test by getting(JvmTestSuite::class) { + description = "Standard unit tests" + } + + + /** Functional tests suite */ + val testFunctional by registering(JvmTestSuite::class) { + description = "Tests that use Gradle TestKit to test functionality" + testType.set(TestSuiteType.FUNCTIONAL_TEST) + + targets.all { + testTask.configure { + shouldRunAfter(test) + } + } + } + + tasks.check { dependsOn(test, testFunctional) } +} + +skipTestFixturesPublications() + +val aggregateTestReports by tasks.registering(TestReport::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + destinationDirectory.set(layout.buildDirectory.dir("reports/tests/aggregated")) + + dependsOn(tasks.withType<AbstractTestTask>()) + + // hardcoded dirs is a bit of a hack, but a fileTree just didn't work + testResults.from("$buildDir_/test-results/test/binary") + testResults.from("$buildDir_/test-results/testFunctional/binary") + testResults.from("$buildDir_/test-results/testIntegration/binary") + + doLast { + logger.lifecycle("Aggregated test report: file://${destinationDirectory.asFile.get()}/index.html") + } +} + +binaryCompatibilityValidator { + ignoredMarkers.add("org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi") +} + +val dokkatooVersion = provider { project.version.toString() } + +val dokkatooConstantsProperties = objects.mapProperty<String, String>().apply { + put("DOKKATOO_VERSION", dokkatooVersion) + put("DOKKA_VERSION", libs.versions.kotlin.dokka) +} + +val buildConfigFileContents: Provider<TextResource> = + dokkatooConstantsProperties.map { constants -> + + val vals = constants.entries + .sortedBy { it.key } + .joinToString("\n") { (k, v) -> + """const val $k = "$v"""" + }.prependIndent(" ") + + resources.text.fromString( + """ + |package org.jetbrains.dokka.dokkatoo.internal + | + |@DokkatooInternalApi + |object DokkatooConstants { + |$vals + |} + | + """.trimMargin() + ) + } + +val generateDokkatooConstants by tasks.registering(Sync::class) { + group = project.name + + val buildConfigFileContents = buildConfigFileContents + + from(buildConfigFileContents) { + rename { "DokkatooConstants.kt" } + into("dev/adamko/dokkatoo/internal/") + } + + into(layout.buildDirectory.dir("generated-source/main/kotlin/")) +} + +kotlin.sourceSets.main { + kotlin.srcDir(generateDokkatooConstants.map { it.destinationDir }) +} + +dokkatoo { + dokkatooSourceSets.configureEach { + externalDocumentationLinks.register("gradle") { + // https://docs.gradle.org/current/javadoc/index.html + url("https://docs.gradle.org/${gradle.gradleVersion}/javadoc/") + } + sourceLink { + localDirectory.set(file("src/main/kotlin")) + val relativeProjectPath = projectDir.relativeToOrNull(rootDir)?.invariantSeparatorsPath ?: "" + remoteUrl("https://github.com/adamko-dev/dokkatoo/tree/main/$relativeProjectPath/src/main/kotlin") + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt new file mode 100644 index 00000000..9d67471a --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooBasePlugin.kt @@ -0,0 +1,355 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKATOO_BASE_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKATOO_CATEGORY_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKA_FORMAT_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.KotlinPlatform +import org.jetbrains.dokka.dokkatoo.dokka.parameters.VisibilityModifier +import org.jetbrains.dokka.dokkatoo.internal.* +import org.jetbrains.dokka.dokkatoo.tasks.DokkatooGenerateTask +import org.jetbrains.dokka.dokkatoo.tasks.DokkatooPrepareModuleDescriptorTask +import org.jetbrains.dokka.dokkatoo.tasks.DokkatooTask +import java.io.File +import javax.inject.Inject +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.TaskContainer +import org.gradle.kotlin.dsl.* +import org.gradle.language.base.plugins.LifecycleBasePlugin + +/** + * The base plugin for Dokkatoo. Sets up Dokkatoo and configures default values, but does not + * add any specific config (specifically, it does not create Dokka Publications). + */ +abstract class DokkatooBasePlugin +@DokkatooInternalApi +@Inject +constructor( + private val providers: ProviderFactory, + private val layout: ProjectLayout, + private val objects: ObjectFactory, +) : Plugin<Project> { + + override fun apply(target: Project) { + // apply the lifecycle-base plugin so the clean task is available + target.pluginManager.apply(LifecycleBasePlugin::class) + + val dokkatooExtension = createExtension(target) + + target.tasks.createDokkaLifecycleTasks() + + val configurationAttributes = objects.newInstance<DokkatooConfigurationAttributes>() + + target.dependencies.attributesSchema { + attribute(DOKKATOO_BASE_ATTRIBUTE) + attribute(DOKKATOO_CATEGORY_ATTRIBUTE) + attribute(DOKKA_FORMAT_ATTRIBUTE) + } + + target.configurations.register(dependencyContainerNames.dokkatoo) { + description = "Fetch all Dokkatoo files from all configurations in other subprojects" + asConsumer() + isVisible = false + attributes { + attribute(DOKKATOO_BASE_ATTRIBUTE, configurationAttributes.dokkatooBaseUsage) + } + } + + configureDokkaPublicationsDefaults(dokkatooExtension) + dokkatooExtension.dokkatooSourceSets.configureDefaults( + sourceSetScopeConvention = dokkatooExtension.sourceSetScopeDefault + ) + + target.tasks.withType<DokkatooGenerateTask>().configureEach { + cacheDirectory.convention(dokkatooExtension.dokkatooCacheDirectory) + workerDebugEnabled.convention(false) + workerLogFile.convention(temporaryDir.resolve("dokka-worker.log")) + workerJvmArgs.set( + listOf( + //"-XX:MaxMetaspaceSize=512m", + "-XX:+HeapDumpOnOutOfMemoryError", + "-XX:+AlwaysPreTouch", // https://github.com/gradle/gradle/issues/3093#issuecomment-387259298 + //"-XX:StartFlightRecording=disk=true,name={path.drop(1).map { if (it.isLetterOrDigit()) it else '-' }.joinToString("")},dumponexit=true,duration=30s", + //"-XX:FlightRecorderOptions=repository=$baseDir/jfr,stackdepth=512", + ) + ) + dokkaConfigurationJsonFile.convention(temporaryDir.resolve("dokka-configuration.json")) + } + + target.tasks.withType<DokkatooPrepareModuleDescriptorTask>().configureEach { + moduleName.convention(dokkatooExtension.moduleName) + includes.from(providers.provider { dokkatooExtension.dokkatooSourceSets.flatMap { it.includes } }) + modulePath.convention(dokkatooExtension.modulePath) + } + + target.tasks.withType<DokkatooGenerateTask>().configureEach { + + publicationEnabled.convention(true) + onlyIf("publication must be enabled") { publicationEnabled.getOrElse(true) } + + generator.dokkaSourceSets.addAllLater( + providers.provider { + // exclude suppressed source sets as early as possible, to avoid unnecessary dependency resolution + dokkatooExtension.dokkatooSourceSets.filterNot { it.suppress.get() } + } + ) + + generator.dokkaSourceSets.configureDefaults( + sourceSetScopeConvention = dokkatooExtension.sourceSetScopeDefault + ) + } + + dokkatooExtension.dokkatooSourceSets.configureDefaults( + sourceSetScopeConvention = dokkatooExtension.sourceSetScopeDefault + ) + } + + private fun createExtension(project: Project): DokkatooExtension { + val dokkatooExtension = project.extensions.create<DokkatooExtension>(EXTENSION_NAME).apply { + moduleName.convention(providers.provider { project.name }) + moduleVersion.convention(providers.provider { project.version.toString() }) + modulePath.convention(project.pathAsFilePath()) + konanHome.convention( + providers + .provider { + // konanHome is set into in extraProperties: + // https://github.com/JetBrains/kotlin/blob/v1.9.0/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTargetPreset.kt#L35-L38 + project.extensions.extraProperties.get("konanHome") as? String? + } + .map { File(it) } + ) + + sourceSetScopeDefault.convention(project.path) + dokkatooPublicationDirectory.convention(layout.buildDirectory.dir("dokka")) + dokkatooModuleDirectory.convention(layout.buildDirectory.dir("dokka-module")) + dokkatooConfigurationsDirectory.convention(layout.buildDirectory.dir("dokka-config")) + } + + dokkatooExtension.versions { + jetbrainsDokka.convention(DokkatooConstants.DOKKA_VERSION) + jetbrainsMarkdown.convention("0.3.1") + freemarker.convention("2.3.31") + kotlinxHtml.convention("0.8.0") + kotlinxCoroutines.convention("1.6.4") + } + + return dokkatooExtension + } + + /** Set defaults in all [DokkatooExtension.dokkatooPublications]s */ + private fun configureDokkaPublicationsDefaults( + dokkatooExtension: DokkatooExtension, + ) { + dokkatooExtension.dokkatooPublications.all { + enabled.convention(true) + cacheRoot.convention(dokkatooExtension.dokkatooCacheDirectory) + delayTemplateSubstitution.convention(false) + failOnWarning.convention(false) + finalizeCoroutines.convention(false) + moduleName.convention(dokkatooExtension.moduleName) + moduleVersion.convention(dokkatooExtension.moduleVersion) + offlineMode.convention(false) + outputDir.convention(dokkatooExtension.dokkatooPublicationDirectory) + suppressInheritedMembers.convention(false) + suppressObviousFunctions.convention(true) + } + } + + /** Set conventions for all [DokkaSourceSetSpec] properties */ + private fun NamedDomainObjectContainer<DokkaSourceSetSpec>.configureDefaults( + sourceSetScopeConvention: Property<String>, + ) { + configureEach dss@{ + analysisPlatform.convention(KotlinPlatform.DEFAULT) + displayName.convention( + analysisPlatform.map { platform -> + // Match existing Dokka naming conventions. (This should probably be simplified!) + when { + // Multiplatform source sets (e.g. commonMain, jvmMain, macosMain) + name.endsWith("Main") -> name.substringBeforeLast("Main") + + // indeterminate source sets should be named by the Kotlin platform + else -> platform.displayName + } + } + ) + documentedVisibilities.convention(setOf(VisibilityModifier.PUBLIC)) + jdkVersion.convention(8) + + enableKotlinStdLibDocumentationLink.convention(true) + enableJdkDocumentationLink.convention(true) + enableAndroidDocumentationLink.convention( + analysisPlatform.map { it == KotlinPlatform.AndroidJVM } + ) + + reportUndocumented.convention(false) + skipDeprecated.convention(false) + skipEmptyPackages.convention(true) + sourceSetScope.convention(sourceSetScopeConvention) + + // Manually added sourceSets should not be suppressed by default. dokkatooSourceSets that are + // automatically added by DokkatooKotlinAdapter will have a sensible value for suppress. + suppress.convention(false) + + suppressGeneratedFiles.convention(true) + + sourceLinks.configureEach { + localDirectory.convention(layout.projectDirectory) + remoteLineSuffix.convention("#L") + } + + perPackageOptions.configureEach { + matchingRegex.convention(".*") + suppress.convention(false) + skipDeprecated.convention(false) + reportUndocumented.convention(false) + } + + externalDocumentationLinks { + configureEach { + enabled.convention(true) + packageListUrl.convention(url.map { it.appendPath("package-list") }) + } + + maybeCreate("jdk") { + enabled.convention(this@dss.enableJdkDocumentationLink) + url(this@dss.jdkVersion.map { jdkVersion -> + when { + jdkVersion < 11 -> "https://docs.oracle.com/javase/${jdkVersion}/docs/api/" + else -> "https://docs.oracle.com/en/java/javase/${jdkVersion}/docs/api/" + } + }) + packageListUrl(this@dss.jdkVersion.map { jdkVersion -> + when { + jdkVersion < 11 -> "https://docs.oracle.com/javase/${jdkVersion}/docs/api/package-list" + else -> "https://docs.oracle.com/en/java/javase/${jdkVersion}/docs/api/element-list" + } + }) + } + + maybeCreate("kotlinStdlib") { + enabled.convention(this@dss.enableKotlinStdLibDocumentationLink) + url("https://kotlinlang.org/api/latest/jvm/stdlib/") + } + + maybeCreate("androidSdk") { + enabled.convention(this@dss.enableAndroidDocumentationLink) + url("https://developer.android.com/reference/kotlin/") + } + + maybeCreate("androidX") { + enabled.convention(this@dss.enableAndroidDocumentationLink) + url("https://developer.android.com/reference/kotlin/") + packageListUrl("https://developer.android.com/reference/kotlin/androidx/package-list") + } + } + } + } + + private fun TaskContainer.createDokkaLifecycleTasks() { + register<DokkatooTask>(taskNames.generate) { + description = "Generates Dokkatoo publications for all formats" + dependsOn(withType<DokkatooGenerateTask>()) + } + } + + // workaround for https://github.com/gradle/gradle/issues/23708 + private fun RegularFileProperty.convention(file: File): RegularFileProperty = + convention(objects.fileProperty().fileValue(file)) + + // workaround for https://github.com/gradle/gradle/issues/23708 + private fun RegularFileProperty.convention(file: Provider<File>): RegularFileProperty = + convention(objects.fileProperty().fileProvider(file)) + + companion object { + + const val EXTENSION_NAME = "dokkatoo" + + /** + * The group of all Dokkatoo [Gradle tasks][org.gradle.api.Task]. + * + * @see org.gradle.api.Task.getGroup + */ + const val TASK_GROUP = "dokkatoo" + + /** The names of [Gradle tasks][org.gradle.api.Task] created by Dokkatoo */ + val taskNames = TaskNames(null) + + /** The names of [Configuration]s created by Dokkatoo */ + val dependencyContainerNames = DependencyContainerNames(null) + + internal val jsonMapper = Json { + prettyPrint = true + @OptIn(ExperimentalSerializationApi::class) + prettyPrintIndent = " " + } + } + + @DokkatooInternalApi + abstract class HasFormatName { + abstract val formatName: String? + + /** Appends [formatName] to the end of the string, camelcase style, if [formatName] is not null */ + protected fun String.appendFormat(): String = + when (val name = formatName) { + null -> this + else -> this + name.uppercaseFirstChar() + } + } + + /** + * Names of the Gradle [Configuration]s used by the [Dokkatoo Plugin][DokkatooBasePlugin]. + * + * Beware the confusing terminology: + * - [Gradle Configurations][org.gradle.api.artifacts.Configuration] - share files between subprojects. Each has a name. + * - [DokkaConfiguration][org.jetbrains.dokka.DokkaConfiguration] - parameters for executing the Dokka Generator + */ + @DokkatooInternalApi + class DependencyContainerNames(override val formatName: String?) : HasFormatName() { + + val dokkatoo = "dokkatoo".appendFormat() + + /** Name of the [Configuration] that _consumes_ all [org.jetbrains.dokka.DokkaConfiguration.DokkaModuleDescription] files */ + val dokkatooModuleFilesConsumer = "dokkatooModule".appendFormat() + + /** Name of the [Configuration] that _provides_ all [org.jetbrains.dokka.DokkaConfiguration.DokkaModuleDescription] files to other projects */ + val dokkatooModuleFilesProvider = "dokkatooModuleElements".appendFormat() + + /** + * Classpath used to execute the Dokka Generator. + * + * Extends [dokkaPluginsClasspath], so Dokka plugins and their dependencies are included. + */ + val dokkaGeneratorClasspath = "dokkatooGeneratorClasspath".appendFormat() + + /** Dokka Plugins (including transitive dependencies, so this can be passed to the Dokka Generator Worker classpath) */ + val dokkaPluginsClasspath = "dokkatooPlugin".appendFormat() + + /** + * Dokka Plugins (excluding transitive dependencies) will be used to create Dokka Generator Parameters + * + * Generally, this configuration should not be invoked manually. Instead, use [dokkaPluginsClasspath]. + */ + val dokkaPluginsIntransitiveClasspath = "dokkatooPluginIntransitive".appendFormat() + } + + @DokkatooInternalApi + class TaskNames(override val formatName: String?) : HasFormatName() { + val generate = "dokkatooGenerate".appendFormat() + val generatePublication = "dokkatooGeneratePublication".appendFormat() + val generateModule = "dokkatooGenerateModule".appendFormat() + val prepareModuleDescriptor = "prepareDokkatooModuleDescriptor".appendFormat() + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt new file mode 100644 index 00000000..d7b91541 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooExtension.kt @@ -0,0 +1,130 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.jetbrains.dokka.dokkatoo.internal.* +import java.io.Serializable +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.* + +/** + * Configure the behaviour of the [DokkatooBasePlugin]. + */ +abstract class DokkatooExtension +@DokkatooInternalApi +constructor( + objects: ObjectFactory, +) : ExtensionAware, Serializable { + + /** Directory into which [DokkaPublication]s will be produced */ + abstract val dokkatooPublicationDirectory: DirectoryProperty + + /** Directory into which Dokka Modules will be produced */ + abstract val dokkatooModuleDirectory: DirectoryProperty + + abstract val dokkatooConfigurationsDirectory: DirectoryProperty + + /** Default Dokkatoo cache directory */ + abstract val dokkatooCacheDirectory: DirectoryProperty + + abstract val moduleName: Property<String> + abstract val moduleVersion: Property<String> + abstract val modulePath: Property<String> + + /** + * An arbitrary string used to group source sets that originate from different Gradle subprojects. + * + * This is primarily used by Kotlin Multiplatform projects, which can have multiple source sets + * per subproject. + * + * Defaults to [the path of the subproject][org.gradle.api.Project.getPath]. + */ + abstract val sourceSetScopeDefault: Property<String> + + /** + * The Konan home directory, which contains libraries for Kotlin/Native development. + * + * This is only required as a workaround to fetch the compile-time dependencies in Kotlin/Native + * projects with a version below 2.0. + */ + // This property should be removed when Dokkatoo only supports KGP 2 or higher. + @DokkatooInternalApi + abstract val konanHome: RegularFileProperty + + /** + * Configuration for creating Dokka Publications. + * + * Each publication will generate one Dokka site based on the included Dokka Source Sets. + * + * The type of site is determined by the Dokka Plugins. By default, an HTML site will be generated. + */ + val dokkatooPublications: NamedDomainObjectContainer<DokkaPublication> = + extensions.adding( + "dokkatooPublications", + objects.domainObjectContainer { named -> objects.newInstance(named, pluginsConfiguration) } + ) + + /** + * Dokka Source Sets describe the source code that should be included in a Dokka Publication. + * + * Dokka will not generate documentation unless there is at least there is at least one Dokka Source Set. + * + * TODO make sure dokkatooSourceSets doc is up to date... + * + * Only source sets that are contained within _this project_ should be included here. + * To merge source sets from other projects, use the Gradle dependencies block. + * + * ```kotlin + * dependencies { + * // merge :other-project into this project's Dokka Configuration + * dokka(project(":other-project")) + * } + * ``` + * + * Or, to include other Dokka Publications as a Dokka Module use + * + * ```kotlin + * dependencies { + * // include :other-project as a module in this project's Dokka Configuration + * dokkaModule(project(":other-project")) + * } + * ``` + * + * Dokka will merge Dokka Source Sets from other subprojects if... + */ + val dokkatooSourceSets: NamedDomainObjectContainer<DokkaSourceSetSpec> = + extensions.adding("dokkatooSourceSets", objects.domainObjectContainer()) + + /** + * Dokka Plugin are used to configure the way Dokka generates a format. + * Some plugins can be configured via parameters, and those parameters are stored in this + * container. + */ + val pluginsConfiguration: DokkaPluginParametersContainer = + extensions.adding("pluginsConfiguration", objects.dokkaPluginParametersContainer()) + + /** + * Versions of dependencies that Dokkatoo will use to run Dokka Generator. + * + * These versions can be set to change the versions of dependencies that Dokkatoo uses defaults, + * or can be read to align versions. + */ + val versions: Versions = extensions.adding("versions", objects.newInstance()) + + interface Versions : ExtensionAware { + + /** Default version used for Dokka dependencies */ + val jetbrainsDokka: Property<String> + val jetbrainsMarkdown: Property<String> + val freemarker: Property<String> + val kotlinxHtml: Property<String> + val kotlinxCoroutines: Property<String> + + companion object + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooPlugin.kt new file mode 100644 index 00000000..0ace2ca6 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/DokkatooPlugin.kt @@ -0,0 +1,32 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.formats.DokkatooGfmPlugin +import org.jetbrains.dokka.dokkatoo.formats.DokkatooHtmlPlugin +import org.jetbrains.dokka.dokkatoo.formats.DokkatooJavadocPlugin +import org.jetbrains.dokka.dokkatoo.formats.DokkatooJekyllPlugin +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.* + +/** + * Dokkatoo Gradle Plugin. + * + * Creates all necessary defaults to generate documentation for HTML, Jekyll, Markdown, and Javadoc formats. + */ +abstract class DokkatooPlugin +@DokkatooInternalApi +constructor() : Plugin<Project> { + + override fun apply(target: Project) { + with(target.pluginManager) { + apply(type = DokkatooBasePlugin::class) + + // auto-apply the custom format plugins + apply(type = DokkatooGfmPlugin::class) + apply(type = DokkatooHtmlPlugin::class) + apply(type = DokkatooJavadocPlugin::class) + apply(type = DokkatooJekyllPlugin::class) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt new file mode 100644 index 00000000..f5261bb4 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooAndroidAdapter.kt @@ -0,0 +1,214 @@ +package org.jetbrains.dokka.dokkatoo.adapters + +import com.android.build.api.dsl.CommonExtension +import com.android.build.gradle.AppExtension +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.TestExtension +import com.android.build.gradle.api.BaseVariant +import com.android.build.gradle.internal.dependency.VariantDependencies +import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.CLASSES_JAR +import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.PROCESSED_JAR +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.dokka.parameters.KotlinPlatform +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.collectIncomingFiles +import javax.inject.Inject +import org.gradle.api.DomainObjectSet +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logging +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ProviderFactory +import org.gradle.kotlin.dsl.* + +@DokkatooInternalApi +abstract class DokkatooAndroidAdapter @Inject constructor( + private val objects: ObjectFactory, +) : Plugin<Project> { + + override fun apply(project: Project) { + logger.info("applied DokkatooAndroidAdapter to ${project.path}") + + project.plugins.withType<DokkatooBasePlugin>().configureEach { + project.pluginManager.apply { + withPlugin("com.android.base") { configure(project) } + withPlugin("com.android.application") { configure(project) } + withPlugin("com.android.library") { configure(project) } + } + } + } + + protected fun configure(project: Project) { + val dokkatooExtension = project.extensions.getByType<DokkatooExtension>() + + val androidExt = AndroidExtensionWrapper(project) + + if (androidExt == null) { + logger.warn("DokkatooAndroidAdapter could not get Android Extension for project ${project.path}") + return + } + + dokkatooExtension.dokkatooSourceSets.configureEach { + + classpath.from( + analysisPlatform.map { analysisPlatform -> + when (analysisPlatform) { + KotlinPlatform.AndroidJVM -> + AndroidClasspathCollector( + androidExt = androidExt, + configurations = project.configurations, + objects = objects, + ) + + else -> + objects.fileCollection() + } + } + ) + } + } + + @DokkatooInternalApi + companion object { + private val logger = Logging.getLogger(DokkatooAndroidAdapter::class.java) + } +} + +private fun AndroidExtensionWrapper( + project: Project +): AndroidExtensionWrapper? { + +// fetching _all_ configuration names is very brute force and should probably be refined to +// only fetch those that match a specific DokkaSourceSetSpec + + return runCatching { + val androidExt = project.extensions.getByType<BaseExtension>() + AndroidExtensionWrapper.forBaseExtension( + androidExt = androidExt, + providers = project.providers, + objects = project.objects + ) + }.recoverCatching { + val androidExt = project.extensions.getByType(CommonExtension::class) + AndroidExtensionWrapper.forCommonExtension(androidExt) + }.getOrNull() +} + +/** + * Android Gradle Plugin is having a refactor. Try to wrap the Android extension so that Dokkatoo + * can still access the configuration names without caring about which AGP version is in use. + */ +private interface AndroidExtensionWrapper { + fun variantConfigurationNames(): Set<String> + + companion object { + + @Suppress("DEPRECATION") + fun forBaseExtension( + androidExt: BaseExtension, + providers: ProviderFactory, + objects: ObjectFactory, + ): AndroidExtensionWrapper { + return object : AndroidExtensionWrapper { + /** Fetch all configuration names used by all variants. */ + override fun variantConfigurationNames(): Set<String> { + val collector = objects.domainObjectSet(BaseVariant::class) + + val variants: DomainObjectSet<BaseVariant> = + collector.apply { + addAllLater(providers.provider { + when (androidExt) { + is LibraryExtension -> androidExt.libraryVariants + is AppExtension -> androidExt.applicationVariants + is TestExtension -> androidExt.applicationVariants + else -> emptyList() + } + }) + } + + return buildSet { + variants.forEach { + add(it.compileConfiguration.name) + add(it.runtimeConfiguration.name) + add(it.annotationProcessorConfiguration.name) + } + } + } + } + } + + fun forCommonExtension( + androidExt: CommonExtension<*, *, *, *> + ): AndroidExtensionWrapper { + return object : AndroidExtensionWrapper { + /** Fetch all configuration names used by all variants. */ + override fun variantConfigurationNames(): Set<String> { + return buildSet { + @Suppress("UnstableApiUsage") + androidExt.sourceSets.forEach { + add(it.apiConfigurationName) + add(it.compileOnlyConfigurationName) + add(it.implementationConfigurationName) + add(it.runtimeOnlyConfigurationName) + add(it.wearAppConfigurationName) + add(it.annotationProcessorConfigurationName) + } + } + } + } + } + } +} + + +/** + * A utility for determining the classpath of an Android compilation. + * + * It's important that this class is separate from [DokkatooAndroidAdapter]. It must be separate + * because it uses Android Gradle Plugin classes (like [BaseExtension]). Were it not separate, and + * these classes were present in the function signatures of [DokkatooAndroidAdapter], then when + * Gradle tries to create a decorated instance of [DokkatooAndroidAdapter] it will if the project + * does not have the Android Gradle Plugin applied, because the classes will be missing. + */ +private object AndroidClasspathCollector { + + operator fun invoke( + androidExt: AndroidExtensionWrapper, + configurations: ConfigurationContainer, + objects: ObjectFactory, + ): FileCollection { + val compilationClasspath = objects.fileCollection() + + fun collectConfiguration(named: String) { + listOf( + // need to fetch multiple different types of files, because AGP is weird and doesn't seem + // to have a 'just give me normal JVM classes' option + ARTIFACT_TYPE_ATTRIBUTE to PROCESSED_JAR.type, + ARTIFACT_TYPE_ATTRIBUTE to CLASSES_JAR.type, + ).forEach { (attribute, attributeValue) -> + configurations.collectIncomingFiles(named, collector = compilationClasspath) { + attributes { + attribute(attribute, attributeValue) + } + lenient(true) + } + } + } + + // fetch android.jar + collectConfiguration(named = VariantDependencies.CONFIG_NAME_ANDROID_APIS) + + val variantConfigurations = androidExt.variantConfigurationNames() + + for (variantConfig in variantConfigurations) { + collectConfiguration(named = variantConfig) + } + + return compilationClasspath + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooJavaAdapter.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooJavaAdapter.kt new file mode 100644 index 00000000..0f834363 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooJavaAdapter.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka.dokkatoo.adapters + +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.logging.Logging +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.* + +/** + * Apply Java specific configuration to the Dokkatoo plugin. + * + * **Must be applied *after* [org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin]** + */ +@DokkatooInternalApi +abstract class DokkatooJavaAdapter @Inject constructor() : Plugin<Project> { + + private val logger = Logging.getLogger(this::class.java) + + override fun apply(project: Project) { + logger.info("applied DokkatooJavaAdapter to ${project.path}") + + // wait for the Java plugin to be applied + project.plugins.withType<JavaBasePlugin>().configureEach { + + // fetch the toolchain, and use the language version as Dokka's jdkVersion + val toolchainLanguageVersion = project.extensions.getByType<JavaPluginExtension>() + .toolchain + .languageVersion + + val dokka = project.extensions.getByType<DokkatooExtension>() + dokka.dokkatooSourceSets.configureEach { + jdkVersion.set(toolchainLanguageVersion.map { it.asInt() }.orElse(8)) + } + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt new file mode 100644 index 00000000..82df651d --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/adapters/DokkatooKotlinAdapter.kt @@ -0,0 +1,459 @@ +package org.jetbrains.dokka.dokkatoo.adapters + +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.api.LibraryVariant +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.adapters.DokkatooKotlinAdapter.Companion.currentKotlinToolingVersion +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetIdSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetIdSpec.Companion.dokkaSourceSetIdSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.KotlinPlatform +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.not +import java.io.File +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.logging.Logging +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.SetProperty +import org.gradle.kotlin.dsl.* +import org.jetbrains.kotlin.commonizer.KonanDistribution +import org.jetbrains.kotlin.commonizer.platformLibsDir +import org.jetbrains.kotlin.commonizer.stdlib +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion +import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataCompilation +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion + +/** + * The [DokkatooKotlinAdapter] plugin will automatically register Kotlin source sets as Dokka source sets. + * + * This is not a standalone plugin, it requires [org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin] is also applied. + */ +@DokkatooInternalApi +abstract class DokkatooKotlinAdapter @Inject constructor( + private val objects: ObjectFactory, + private val providers: ProviderFactory, +) : Plugin<Project> { + + override fun apply(project: Project) { + logger.info("applied DokkatooKotlinAdapter to ${project.path}") + + project.plugins.withType<DokkatooBasePlugin>().configureEach { + project.pluginManager.apply { + withPlugin("org.jetbrains.kotlin.android") { exec(project) } + withPlugin("org.jetbrains.kotlin.js") { exec(project) } + withPlugin("org.jetbrains.kotlin.jvm") { exec(project) } + withPlugin("org.jetbrains.kotlin.multiplatform") { exec(project) } + } + } + } + + private fun exec(project: Project) { + val kotlinExtension = project.extensions.findKotlinExtension() ?: run { + logger.info("could not find Kotlin Extension") + return + } + logger.info("Configuring Dokkatoo in Gradle Kotlin Project ${project.path}") + + val dokkatooExtension = project.extensions.getByType<DokkatooExtension>() + + // first fetch the relevant properties of all KotlinCompilations + val compilationDetailsBuilder = KotlinCompilationDetailsBuilder( + providers = providers, + objects = objects, + konanHome = dokkatooExtension.konanHome.asFile, + ) + val allKotlinCompilationDetails: ListProperty<KotlinCompilationDetails> = + compilationDetailsBuilder.createCompilationDetails( + kotlinProjectExtension = kotlinExtension, + ) + + // second, fetch the relevant properties of the Kotlin source sets + val sourceSetDetailsBuilder = KotlinSourceSetDetailsBuilder( + providers = providers, + objects = objects, + sourceSetScopeDefault = dokkatooExtension.sourceSetScopeDefault, + projectPath = project.path, + ) + val sourceSetDetails: NamedDomainObjectContainer<KotlinSourceSetDetails> = + sourceSetDetailsBuilder.createSourceSetDetails( + kotlinSourceSets = kotlinExtension.sourceSets, + allKotlinCompilationDetails = allKotlinCompilationDetails, + ) + + // for each Kotlin source set, register a Dokkatoo source set + registerDokkatooSourceSets( + dokkatooExtension = dokkatooExtension, + sourceSetDetails = sourceSetDetails, + ) + } + + /** Register a [DokkaSourceSetSpec] for each element in [sourceSetDetails] */ + private fun registerDokkatooSourceSets( + dokkatooExtension: DokkatooExtension, + sourceSetDetails: NamedDomainObjectContainer<KotlinSourceSetDetails>, + ) { + // proactively use 'all' so source sets will be available in users' build files if they use `named("...")` + sourceSetDetails.all details@{ + dokkatooExtension.dokkatooSourceSets.register(details = this@details) + } + } + + /** Register a single [DokkaSourceSetSpec] for [details] */ + private fun NamedDomainObjectContainer<DokkaSourceSetSpec>.register( + details: KotlinSourceSetDetails + ) { + val kssPlatform = details.compilations.map { values: List<KotlinCompilationDetails> -> + values.map { it.kotlinPlatform } + .distinct() + .singleOrNull() ?: KotlinPlatform.Common + } + + val kssClasspath = determineClasspath(details) + + register(details.name) dss@{ + suppress.set(!details.isPublishedSourceSet()) + sourceRoots.from(details.sourceDirectories) + classpath.from(kssClasspath) + analysisPlatform.set(kssPlatform) + dependentSourceSets.addAllLater(details.dependentSourceSetIds) + } + } + + private fun determineClasspath( + details: KotlinSourceSetDetails + ): Provider<FileCollection> { + return details.compilations.map { compilations: List<KotlinCompilationDetails> -> + val classpath = objects.fileCollection() + + if (compilations.isNotEmpty()) { + compilations.fold(classpath) { acc, compilation -> + acc.from(compilation.compilationClasspath) + // can't use compileDependencyFiles, it causes weird dependency resolution errors in Android projects + //acc.from(providers.provider { compilation.compileDependencyFiles }) + } + } else { + classpath + .from(details.sourceDirectories) + .from(details.sourceDirectoriesOfDependents) + } + } + } + + @DokkatooInternalApi + companion object { + private val logger = Logging.getLogger(DokkatooKotlinAdapter::class.java) + + /** Try and get [KotlinProjectExtension], or `null` if it's not present */ + private fun ExtensionContainer.findKotlinExtension(): KotlinProjectExtension? = + try { + findByType() + // fallback to trying to get the JVM extension + // (not sure why I did this... maybe to be compatible with really old versions?) + ?: findByType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>() + } catch (e: Throwable) { + when (e) { + is TypeNotPresentException, + is ClassNotFoundException, + is NoClassDefFoundError -> null + + else -> throw e + } + } + + /** Get the version of the Kotlin Gradle Plugin currently used to compile the project */ + // Must be lazy, else tests fail (because the KGP plugin isn't accessible) + internal val currentKotlinToolingVersion: KotlinToolingVersion by lazy { + val kgpVersion = getKotlinPluginVersion(logger) + KotlinToolingVersion(kgpVersion) + } + } +} + + +/** + * Store the details of all [KotlinCompilation]s in a configuration cache compatible way. + * + * The compilation details may come from a multiplatform project ([KotlinMultiplatformExtension]) + * or a single-platform project ([KotlinSingleTargetExtension]). + */ +@DokkatooInternalApi +private data class KotlinCompilationDetails( + val target: String, + val kotlinPlatform: KotlinPlatform, + val allKotlinSourceSetsNames: Set<String>, + val publishedCompilation: Boolean, + val dependentSourceSetNames: Set<String>, + val compilationClasspath: FileCollection, + val defaultSourceSetName: String, +) + +/** Utility class, encapsulating logic for building [KotlinCompilationDetails] */ +private class KotlinCompilationDetailsBuilder( + private val objects: ObjectFactory, + private val providers: ProviderFactory, + private val konanHome: Provider<File>, +) { + + fun createCompilationDetails( + kotlinProjectExtension: KotlinProjectExtension, + ): ListProperty<KotlinCompilationDetails> { + + val details = objects.listProperty<KotlinCompilationDetails>() + + details.addAll( + providers.provider { + kotlinProjectExtension + .allKotlinCompilations() + .map { compilation -> + createCompilationDetails(compilation = compilation) + } + }) + + return details + } + + /** Create a single [KotlinCompilationDetails] for [compilation] */ + private fun createCompilationDetails( + compilation: KotlinCompilation<*>, + ): KotlinCompilationDetails { + val allKotlinSourceSetsNames = + compilation.allKotlinSourceSets.map { it.name } + compilation.defaultSourceSet.name + + val dependentSourceSetNames = + compilation.defaultSourceSet.dependsOn.map { it.name } + + val compilationClasspath: FileCollection = + collectKotlinCompilationClasspath(compilation = compilation) + + return KotlinCompilationDetails( + target = compilation.target.name, + kotlinPlatform = KotlinPlatform.fromString(compilation.platformType.name), + allKotlinSourceSetsNames = allKotlinSourceSetsNames.toSet(), + publishedCompilation = compilation.isPublished(), + dependentSourceSetNames = dependentSourceSetNames.toSet(), + compilationClasspath = compilationClasspath, + defaultSourceSetName = compilation.defaultSourceSet.name + ) + } + + private fun KotlinProjectExtension.allKotlinCompilations(): Collection<KotlinCompilation<*>> = + when (this) { + is KotlinMultiplatformExtension -> targets.flatMap { it.compilations } + is KotlinSingleTargetExtension<*> -> target.compilations + else -> emptyList() // shouldn't happen? + } + + /** + * Get the [Configuration][org.gradle.api.artifacts.Configuration] names of all configurations + * used to build this [KotlinCompilation] and + * [its source sets][KotlinCompilation.kotlinSourceSets]. + */ + private fun collectKotlinCompilationClasspath( + compilation: KotlinCompilation<*>, + ): FileCollection { + val compilationClasspath = objects.fileCollection() + + // collect dependency files from 'regular' Kotlin compilations + compilationClasspath.from(providers.provider { compilation.compileDependencyFiles }) + + // apply workaround for Kotlin/Native, which will be fixed in Kotlin 2.0 + // (see KT-61559: K/N dependencies will be part of `compilation.compileDependencyFiles`) + if ( + currentKotlinToolingVersion < KotlinToolingVersion("2.0.0") + && + compilation is AbstractKotlinNativeCompilation + ) { + compilationClasspath.from( + konanHome.map { konanHome -> + kotlinNativeDependencies(konanHome, compilation.konanTarget) + } + ) + } + + return compilationClasspath + } + + private fun kotlinNativeDependencies(konanHome: File, target: KonanTarget): FileCollection { + val konanDistribution = KonanDistribution(konanHome) + + val dependencies = objects.fileCollection() + + dependencies.from(konanDistribution.stdlib) + + // Konan library files for a specific target + dependencies.from( + konanDistribution.platformLibsDir + .resolve(target.name) + .listFiles() + .orEmpty() + .filter { it.isDirectory || it.extension == "klib" } + ) + + return dependencies + } + + companion object { + + /** + * Determine if a [KotlinCompilation] is 'publishable', and so should be enabled by default + * when creating a Dokka publication. + * + * Typically, 'main' compilations are publishable and 'test' compilations should be suppressed. + * This can be overridden manually, though. + * + * @see DokkaSourceSetSpec.suppress + */ + private fun KotlinCompilation<*>.isPublished(): Boolean { + return when (this) { + is KotlinMetadataCompilation<*> -> true + + is KotlinJvmAndroidCompilation -> + androidVariant is LibraryVariant || androidVariant is ApplicationVariant + + else -> + name == MAIN_COMPILATION_NAME + } + } + } +} + + +/** + * Store the details of all [KotlinSourceSet]s in a configuration cache compatible way. + * + * @param[named] Should be [KotlinSourceSet.getName] + */ +@DokkatooInternalApi +private abstract class KotlinSourceSetDetails @Inject constructor( + private val named: String, +) : Named { + + /** Direct source sets that this source set depends on */ + abstract val dependentSourceSetIds: SetProperty<DokkaSourceSetIdSpec> + abstract val sourceDirectories: ConfigurableFileCollection + /** _All_ source directories from any (recursively) dependant source set */ + abstract val sourceDirectoriesOfDependents: ConfigurableFileCollection + /** The specific compilations used to build this source set */ + abstract val compilations: ListProperty<KotlinCompilationDetails> + + /** Estimate if this Kotlin source set contains 'published' sources */ + fun isPublishedSourceSet(): Provider<Boolean> = + compilations.map { values -> + values.any { it.publishedCompilation } + } + + override fun getName(): String = named +} + +/** Utility class, encapsulating logic for building [KotlinCompilationDetails] */ +private class KotlinSourceSetDetailsBuilder( + private val sourceSetScopeDefault: Provider<String>, + private val objects: ObjectFactory, + private val providers: ProviderFactory, + /** Used for logging */ + private val projectPath: String, +) { + + private val logger = Logging.getLogger(KotlinSourceSetDetails::class.java) + + fun createSourceSetDetails( + kotlinSourceSets: NamedDomainObjectContainer<KotlinSourceSet>, + allKotlinCompilationDetails: ListProperty<KotlinCompilationDetails>, + ): NamedDomainObjectContainer<KotlinSourceSetDetails> { + + val sourceSetDetails = objects.domainObjectContainer(KotlinSourceSetDetails::class) + + kotlinSourceSets.configureEach kss@{ + sourceSetDetails.register( + kotlinSourceSet = this, + allKotlinCompilationDetails = allKotlinCompilationDetails, + ) + } + + return sourceSetDetails + } + + private fun NamedDomainObjectContainer<KotlinSourceSetDetails>.register( + kotlinSourceSet: KotlinSourceSet, + allKotlinCompilationDetails: ListProperty<KotlinCompilationDetails>, + ) { + + // TODO: Needs to respect filters. + // We probably need to change from "sourceRoots" to support "sourceFiles" + // https://github.com/Kotlin/dokka/issues/1215 + val extantSourceDirectories = providers.provider { + kotlinSourceSet.kotlin.sourceDirectories.filter { it.exists() } + } + + val compilations = allKotlinCompilationDetails.map { allCompilations -> + allCompilations.filter { compilation -> + kotlinSourceSet.name in compilation.allKotlinSourceSetsNames + } + } + + // determine the source sets IDs of _other_ source sets that _this_ source depends on. + val dependentSourceSets = providers.provider { kotlinSourceSet.dependsOn } + val dependentSourceSetIds = + providers.zip( + dependentSourceSets, + sourceSetScopeDefault, + ) { sourceSets, sourceSetScope -> + logger.info("[$projectPath] source set ${kotlinSourceSet.name} has ${sourceSets.size} dependents ${sourceSets.joinToString { it.name }}") + sourceSets.map { dependedKss -> + objects.dokkaSourceSetIdSpec(sourceSetScope, dependedKss.name) + } + } + + val sourceDirectoriesOfDependents = providers.provider { + kotlinSourceSet + .allDependentSourceSets() + .fold(objects.fileCollection()) { acc, sourceSet -> + acc.from(sourceSet.kotlin.sourceDirectories) + } + } + + register(kotlinSourceSet.name) { + this.dependentSourceSetIds.addAll(dependentSourceSetIds) + this.sourceDirectories.from(extantSourceDirectories) + this.sourceDirectoriesOfDependents.from(sourceDirectoriesOfDependents) + this.compilations.addAll(compilations) + } + } + + /** + * Return a list containing _all_ source sets that this source set depends on, + * searching recursively. + * + * @see KotlinSourceSet.dependsOn + */ + private tailrec fun KotlinSourceSet.allDependentSourceSets( + queue: Set<KotlinSourceSet> = dependsOn.toSet(), + allDependents: List<KotlinSourceSet> = emptyList(), + ): List<KotlinSourceSet> { + val next = queue.firstOrNull() ?: return allDependents + return next.allDependentSourceSets( + queue = (queue - next) union next.dependsOn, + allDependents = allDependents + next, + ) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/distributions/DokkatooConfigurationAttributes.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/distributions/DokkatooConfigurationAttributes.kt new file mode 100644 index 00000000..57ca5ef9 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/distributions/DokkatooConfigurationAttributes.kt @@ -0,0 +1,59 @@ +package org.jetbrains.dokka.dokkatoo.distributions + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Usage +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.* + +/** + * Gradle Configuration Attributes for sharing Dokkatoo files across subprojects. + * + * These attributes are used to tag [Configuration]s, so files can be shared between subprojects. + */ +@DokkatooInternalApi +abstract class DokkatooConfigurationAttributes +@Inject +constructor( + objects: ObjectFactory, +) { + + /** A general attribute for all [Configuration]s that are used by the Dokka Gradle plugin */ + val dokkatooBaseUsage: DokkatooBaseAttribute = objects.named("dokkatoo") + + /** for [Configuration]s that provide or consume Dokka parameter files */ + val dokkaParameters: DokkatooCategoryAttribute = objects.named("generator-parameters") + + /** for [Configuration]s that provide or consume Dokka Module files */ + val dokkaModuleFiles: DokkatooCategoryAttribute = objects.named("module-files") +// val dokkaModuleSource: DokkatooCategoryAttribute = objects.named("module-source") + + val dokkaGeneratorClasspath: DokkatooCategoryAttribute = objects.named("generator-classpath") + + val dokkaPluginsClasspath: DokkatooCategoryAttribute = objects.named("plugins-classpath") + + @DokkatooInternalApi + interface DokkatooBaseAttribute : Usage + + @DokkatooInternalApi + interface DokkatooCategoryAttribute : Named + + @DokkatooInternalApi + interface DokkaFormatAttribute : Named + + @DokkatooInternalApi + companion object { + val DOKKATOO_BASE_ATTRIBUTE = + Attribute<DokkatooBaseAttribute>("org.jetbrains.dokka.dokkatoo.base") + val DOKKATOO_CATEGORY_ATTRIBUTE = + Attribute<DokkatooCategoryAttribute>("org.jetbrains.dokka.dokkatoo.category") + val DOKKA_FORMAT_ATTRIBUTE = + Attribute<DokkaFormatAttribute>("org.jetbrains.dokka.dokkatoo.format") + + private inline fun <reified T> Attribute(name: String): Attribute<T> = + Attribute.of(name, T::class.java) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt new file mode 100644 index 00000000..50c26415 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/DokkaPublication.kt @@ -0,0 +1,122 @@ +package org.jetbrains.dokka.dokkatoo.dokka + +import org.jetbrains.dokka.dokkatoo.internal.DokkaPluginParametersContainer +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.adding +import java.io.Serializable +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE +import org.gradle.kotlin.dsl.* + +/** + * A [DokkaPublication] describes a single Dokka output. + * + * Each Publication has its own set of Gradle tasks and [org.gradle.api.artifacts.Configuration]s. + * + * The type of site is determined by the Dokka Plugins. By default, an HTML site will be generated. + * By default, Dokka will create publications for HTML, Jekyll, and GitHub Flavoured Markdown. + */ +abstract class DokkaPublication +@DokkatooInternalApi +@Inject +constructor( + @get:Internal + val formatName: String, + + /** + * Configurations for Dokka Generator Plugins. Must be provided from + * [org.jetbrains.dokka.dokkatoo.DokkatooExtension.pluginsConfiguration]. + */ + pluginsConfiguration: DokkaPluginParametersContainer, +) : Named, Serializable, ExtensionAware { + + /** Configurations for Dokka Generator Plugins. */ + @get:Nested + val pluginsConfiguration: DokkaPluginParametersContainer = + extensions.adding("pluginsConfiguration", pluginsConfiguration) + + @Internal + override fun getName(): String = formatName + + @get:Input + abstract val enabled: Property<Boolean> + + @get:Input + abstract val moduleName: Property<String> + + @get:Input + @get:Optional + abstract val moduleVersion: Property<String> + + @get:Internal + // marked as Internal because this task does not use the directory contents, only the location + abstract val outputDir: DirectoryProperty + + /** + * Because [outputDir] must be [Internal] (so Gradle doesn't check the directory contents), + * [outputDirPath] is required so Gradle can determine if the task is up-to-date. + */ + @get:Input + // marked as an Input because a DokkaPublication is used to configure the appropriate + // DokkatooTasks, which will then + @DokkatooInternalApi + protected val outputDirPath: Provider<String> + get() = outputDir.map { it.asFile.invariantSeparatorsPath } + + @get:Internal + // Marked as Internal because this task does not use the directory contents, only the location. + // Note that `cacheRoot` is not used by Dokka, and will probably be deprecated. + abstract val cacheRoot: DirectoryProperty + + /** + * Because [cacheRoot] must be [Internal] (so Gradle doesn't check the directory contents), + * [cacheRootPath] is required so Gradle can determine if the task is up-to-date. + */ + @get:Input + @get:Optional + @DokkatooInternalApi + protected val cacheRootPath: Provider<String> + get() = cacheRoot.map { it.asFile.invariantSeparatorsPath } + + @get:Input + abstract val offlineMode: Property<Boolean> + +// /** Dokka Configuration files from other subprojects that will be merged into this Dokka Configuration */ +// @get:InputFiles +// @get:NormalizeLineEndings +// @get:PathSensitive(PathSensitivity.NAME_ONLY) +// abstract val dokkaSubprojectConfigurations: ConfigurableFileCollection + +// /** Dokka Module Configuration from other subprojects. */ +// @get:InputFiles +// @get:NormalizeLineEndings +// @get:PathSensitive(PathSensitivity.NAME_ONLY) +// abstract val dokkaModuleDescriptorFiles: ConfigurableFileCollection + + @get:Input + abstract val failOnWarning: Property<Boolean> + + @get:Input + abstract val delayTemplateSubstitution: Property<Boolean> + + @get:Input + abstract val suppressObviousFunctions: Property<Boolean> + + @get:InputFiles + @get:PathSensitive(RELATIVE) + abstract val includes: ConfigurableFileCollection + + @get:Input + abstract val suppressInheritedMembers: Property<Boolean> + + @get:Input + // TODO probably not needed any more, since Dokka Generator now runs in an isolated JVM process + abstract val finalizeCoroutines: Property<Boolean> +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpec.kt new file mode 100644 index 00000000..e91721aa --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpec.kt @@ -0,0 +1,120 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.Serializable +import java.net.URI +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.intellij.lang.annotations.Language + +/** + * Configuration builder that allows creating links leading to externally hosted + * documentation of your dependencies. + * + * For instance, if you are using types from `kotlinx.serialization`, by default + * they will be unclickable in your documentation, as if unresolved. However, + * since API reference for `kotlinx.serialization` is also built by Dokka and is + * [published on kotlinlang.org](https://kotlinlang.org/api/kotlinx.serialization/), + * you can configure external documentation links for it, allowing Dokka to generate + * documentation links for used types, making them clickable and appear resolved. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * externalDocumentationLink { + * url.set(URI("https://kotlinlang.org/api/kotlinx.serialization/")) + * packageListUrl.set( + * rootProject.projectDir.resolve("serialization.package.list").toURI() + * ) + * } + * ``` + */ +abstract class DokkaExternalDocumentationLinkSpec +@DokkatooInternalApi +@Inject +constructor( + private val name: String +) : Serializable, Named { + + /** + * Root URL of documentation to link with. + * + * Dokka will do its best to automatically find `package-list` for the given URL, and link + * declarations together. + * + * It automatic resolution fails or if you want to use locally cached files instead, + * consider providing [packageListUrl]. + * + * Example: + * + * ```kotlin + * java.net.URI("https://kotlinlang.org/api/kotlinx.serialization/") + * ``` + */ + @get:Input + abstract val url: Property<URI> + + /** + * Set the value of [url]. + * + * @param[value] will be converted to a [URI] + */ + fun url(@Language("http-url-reference") value: String): Unit = + url.set(URI(value)) + + /** + * Set the value of [url]. + * + * @param[value] will be converted to a [URI] + */ + fun url(value: Provider<String>): Unit = + url.set(value.map(::URI)) + + /** + * Specifies the exact location of a `package-list` instead of relying on Dokka + * automatically resolving it. Can also be a locally cached file to avoid network calls. + * + * Example: + * + * ```kotlin + * rootProject.projectDir.resolve("serialization.package.list").toURL() + * ``` + */ + @get:Input + abstract val packageListUrl: Property<URI> + + /** + * Set the value of [packageListUrl]. + * + * @param[value] will be converted to a [URI] + */ + fun packageListUrl(@Language("http-url-reference") value: String): Unit = + packageListUrl.set(URI(value)) + + /** + * Set the value of [packageListUrl]. + * + * @param[value] will be converted to a [URI] + */ + fun packageListUrl(value: Provider<String>): Unit = + packageListUrl.set(value.map(::URI)) + + /** + * If enabled this link will be passed to the Dokka Generator. + * + * Defaults to `true`. + * + * @see org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec.enableKotlinStdLibDocumentationLink + * @see org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec.enableJdkDocumentationLink + * @see org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec.enableAndroidDocumentationLink + */ + @get:Input + abstract val enabled: Property<Boolean> + + @Internal + override fun getName(): String = name +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaGeneratorParametersSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaGeneratorParametersSpec.kt new file mode 100644 index 00000000..41090e65 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaGeneratorParametersSpec.kt @@ -0,0 +1,93 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkaPluginParametersContainer +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.adding +import org.jetbrains.dokka.dokkatoo.internal.domainObjectContainer +import javax.inject.Inject +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE +import org.gradle.work.NormalizeLineEndings + +/** + * Parameters used to run Dokka Generator to produce either a Publication or a Module. + * + * + */ +abstract class DokkaGeneratorParametersSpec +@DokkatooInternalApi +@Inject +constructor( + objects: ObjectFactory, + /** + * Configurations for Dokka Generator Plugins. Must be provided from + * [org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication.pluginsConfiguration]. + */ + @get:Nested + val pluginsConfiguration: DokkaPluginParametersContainer, +) : ExtensionAware { + +// /** Dokka Configuration files from other subprojects that will be merged into this Dokka Configuration */ +// @get:InputFiles +// //@get:NormalizeLineEndings +// @get:PathSensitive(PathSensitivity.RELATIVE) +// @get:Optional +// abstract val dokkaSubprojectParameters: ConfigurableFileCollection + + @get:Input + abstract val failOnWarning: Property<Boolean> + + @get:Input + abstract val finalizeCoroutines: Property<Boolean> + + @get:Input + abstract val moduleName: Property<String> + + @get:Input + @get:Optional + abstract val moduleVersion: Property<String> + + @get:Input + abstract val offlineMode: Property<Boolean> + + @get:Input + abstract val suppressObviousFunctions: Property<Boolean> + + @get:Input + abstract val suppressInheritedMembers: Property<Boolean> + + @get:InputFiles + @get:PathSensitive(RELATIVE) + abstract val includes: ConfigurableFileCollection + + /** + * Classpath that contains the Dokka Generator Plugins used to modify this publication. + * + * The plugins should be configured in [org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication.pluginsConfiguration]. + */ + @get:InputFiles + @get:Classpath + abstract val pluginsClasspath: ConfigurableFileCollection + + /** + * Source sets used to generate a Dokka Module. + * + * The values are not used directly in this task, but they are required to be registered as a + * task input for up-to-date checks + */ + @get:Nested + val dokkaSourceSets: NamedDomainObjectContainer<DokkaSourceSetSpec> = + extensions.adding("dokkaSourceSets", objects.domainObjectContainer()) + + /** Dokka Module files from other subprojects. */ + @get:InputFiles + @get:NormalizeLineEndings + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val dokkaModuleFiles: ConfigurableFileCollection +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaModuleDescriptionSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaModuleDescriptionSpec.kt new file mode 100644 index 00000000..af3e13b0 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaModuleDescriptionSpec.kt @@ -0,0 +1,49 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.jetbrains.dokka.DokkaConfiguration + +/** + * Properties that describe a Dokka Module. + * + * These values are passed into Dokka Generator, which will aggregate all provided Modules into a + * single publication. + */ +@DokkatooInternalApi +abstract class DokkaModuleDescriptionSpec +@DokkatooInternalApi +@Inject constructor( + @get:Input + val moduleName: String, +) : Named { + + /** + * @see DokkaConfiguration.DokkaModuleDescription.sourceOutputDirectory + */ + @get:Input + abstract val sourceOutputDirectory: RegularFileProperty + + /** + * @see DokkaConfiguration.DokkaModuleDescription.includes + */ + @get:Input + abstract val includes: ConfigurableFileCollection + + /** + * File path of the subproject that determines where the Dokka Module will be placed within an + * assembled Dokka Publication. + * + * This must be a relative path, and will be appended to the root Dokka Publication directory. + * + * The Gradle project path will also be accepted ([org.gradle.api.Project.getPath]), and the + * colons `:` will be replaced with file separators `/`. + */ + @get:Input + abstract val projectPath: Property<String> +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaPackageOptionsSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaPackageOptionsSpec.kt new file mode 100644 index 00000000..44e55a74 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaPackageOptionsSpec.kt @@ -0,0 +1,84 @@ +@file:Suppress("FunctionName") + +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.Serializable +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input + +/** + * Configuration builder that allows setting some options for specific packages + * matched by [matchingRegex]. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * tasks.dokkaHtml { + * dokkaSourceSets.configureEach { + * perPackageOption { + * matchingRegex.set(".*internal.*") + * suppress.set(true) + * } + * } + * } + * ``` + */ +abstract class DokkaPackageOptionsSpec +@DokkatooInternalApi +constructor() : + HasConfigurableVisibilityModifiers, + Serializable { + + /** + * Regular expression that is used to match the package. + * + * Default is any string: `.*`. + */ + @get:Input + abstract val matchingRegex: Property<String> + + /** + * Whether this package should be skipped when generating documentation. + * + * Default is `false`. + */ + @get:Input + abstract val suppress: Property<Boolean> + + /** + * Set of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations within a + * specific package, as well as if you want to exclude public declarations and only document internal API. + * + * Can be configured for a whole source set, see [DokkaSourceSetSpec.documentedVisibilities]. + * + * Default is [VisibilityModifier.PUBLIC]. + */ + @get:Input + abstract override val documentedVisibilities: SetProperty<VisibilityModifier> + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be overridden on source set level by setting [DokkaSourceSetSpec.skipDeprecated]. + * + * Default is `false`. + */ + @get:Input + abstract val skipDeprecated: Property<Boolean> + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations from + * this package and without KDocs, after they have been filtered by [documentedVisibilities]. + * + * + * Can be overridden on source set level by setting [DokkaSourceSetSpec.reportUndocumented]. + * + * Default is `false`. + */ + @get:Input + abstract val reportUndocumented: Property<Boolean> +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaParametersKxs.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaParametersKxs.kt new file mode 100644 index 00000000..df790bcb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaParametersKxs.kt @@ -0,0 +1,78 @@ +@file:UseSerializers( + FileAsPathStringSerializer::class, +) + +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.File +import java.nio.file.Paths +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.gradle.kotlin.dsl.* +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaModuleDescriptionImpl + + +// Implementations of DokkaConfiguration interfaces that can be serialized to files. +// Serialization is required because Gradle tasks can only pass data to one-another via files. + + +/** + * Any subproject can be merged into a single Dokka Publication. To do this, first it must create + * a Dokka Module. A [DokkaModuleDescriptionKxs] describes a config file for the Dokka Module that + * describes its content. This config file will be used by any aggregating project to produce + * a Dokka Publication with multiple modules. + * + * Note: this class implements [java.io.Serializable] because it is used as a + * [Gradle Property][org.gradle.api.provider.Property], and Gradle must be able to fingerprint + * property values classes using Java Serialization. + * + * All other configuration data classes also implement [java.io.Serializable] via their parent interfaces. + */ +@Serializable +@DokkatooInternalApi +data class DokkaModuleDescriptionKxs( + /** @see DokkaConfiguration.DokkaModuleDescription.name */ + val name: String, + /** + * Location of the Dokka Module directory for a subproject. + * + * @see DokkaConfiguration.DokkaModuleDescription.sourceOutputDirectory + */ + val sourceOutputDirectory: File, + /** @see DokkaConfiguration.DokkaModuleDescription.includes */ + val includes: Set<File>, + /** @see [org.gradle.api.Project.getPath] */ + val modulePath: String, +) { + internal fun convert() = + DokkaModuleDescriptionImpl( + name = name, + relativePathToOutputDirectory = File(modulePath.removePrefix(":").replace(':', '/')), + includes = includes, + sourceOutputDirectory = sourceOutputDirectory, + ) +} + + +/** + * Serialize a [File] as an absolute, canonical file path, with + * [invariant path separators][invariantSeparatorsPath] + */ +private object FileAsPathStringSerializer : KSerializer<File> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("java.io.File", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): File = + Paths.get(decoder.decodeString()).toFile() + + override fun serialize(encoder: Encoder, value: File): Unit = + encoder.encodeString(value.invariantSeparatorsPath) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceLinkSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceLinkSpec.kt new file mode 100644 index 00000000..c89b8b24 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceLinkSpec.kt @@ -0,0 +1,106 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.Serializable +import java.net.URI +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.intellij.lang.annotations.Language + +/** + * Configuration builder that allows adding a `source` link to each signature + * which leads to [remoteUrl] with a specific line number (configurable by setting [remoteLineSuffix]), + * letting documentation readers find source code for each declaration. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * sourceLink { + * localDirectory.set(projectDir.resolve("src")) + * remoteUrl.set(URI("https://github.com/kotlin/dokka/tree/master/src")) + * remoteLineSuffix.set("#L") + * } + * ``` + */ +abstract class DokkaSourceLinkSpec +@DokkatooInternalApi +constructor() : Serializable { + + /** + * Path to the local source directory. The path must be relative to the root of current project. + * + * This path is used to find relative paths of the source files from which the documentation is built. + * These relative paths are then combined with the base url of a source code hosting service specified with + * the [remoteUrl] property to create source links for each declaration. + * + * Example: + * + * ```kotlin + * projectDir.resolve("src") + * ``` + */ + @get:Internal // changing contents of the directory should not invalidate the task + abstract val localDirectory: DirectoryProperty + + /** + * The relative path to [localDirectory] from the project directory. Declared as an input to invalidate the task if that path changes. + * Should not be used anywhere directly. + */ + @get:Input + @DokkatooInternalApi + protected val localDirectoryPath: Provider<String> + get() = localDirectory.map { it.asFile.invariantSeparatorsPath } + + /** + * URL of source code hosting service that can be accessed by documentation readers, + * like GitHub, GitLab, Bitbucket, etc. This URL will be used to generate + * source code links of declarations. + * + * Example: + * + * ```kotlin + * java.net.URI("https://github.com/username/projectname/tree/master/src")) + * ``` + */ + @get:Input + abstract val remoteUrl: Property<URI> + + /** + * Set the value of [remoteUrl]. + * + * @param[value] will be converted to a [URI] + */ + fun remoteUrl(@Language("http-url-reference") value: String): Unit = + remoteUrl.set(URI(value)) + + /** + * Set the value of [remoteUrl]. + * + * @param[value] will be converted to a [URI] + */ + fun remoteUrl(value: Provider<String>): Unit = + remoteUrl.set(value.map(::URI)) + + /** + * Suffix used to append source code line number to the URL. This will help readers navigate + * not only to the file, but to the specific line number of the declaration. + * + * The number itself will be appended to the specified suffix. For instance, + * if this property is set to `#L` and the line number is 10, resulting URL suffix + * will be `#L10` + * + * Suffixes used by popular services: + * - GitHub: `#L` + * - GitLab: `#L` + * - Bitbucket: `#lines-` + * + * Default is `#L`. + */ + @get:Optional + @get:Input + abstract val remoteLineSuffix: Property<String> +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt new file mode 100644 index 00000000..0248e387 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetIdSpec.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.Serializable +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.kotlin.dsl.* + +abstract class DokkaSourceSetIdSpec +@DokkatooInternalApi +@Inject +constructor( + /** + * Unique identifier of the scope that this source set is placed in. + * Each scope provide only unique source set names. + * + * TODO update this doc - DokkaTask doesn't represent one source set scope anymore + * + * E.g. One DokkaTask inside the Gradle plugin represents one source set scope, since there cannot be multiple + * source sets with the same name. However, a Gradle project will not be a proper scope, since there can be + * multiple DokkaTasks that contain source sets with the same name (but different configuration) + */ + @get:Input + val scopeId: String, + + @get:Input + val sourceSetName: String, +) : Named, Serializable { + + @Internal + override fun getName(): String = "$scopeId/$sourceSetName" + + override fun toString(): String = "DokkaSourceSetIdSpec($scopeId/$sourceSetName)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DokkaSourceSetIdSpec) return false + + if (scopeId != other.scopeId) return false + return sourceSetName == other.sourceSetName + } + + override fun hashCode(): Int { + var result = scopeId.hashCode() + result = 31 * result + sourceSetName.hashCode() + return result + } + + companion object { + + /** Utility for creating a new [DokkaSourceSetIdSpec] instance using [ObjectFactory.newInstance] */ + @DokkatooInternalApi + fun ObjectFactory.dokkaSourceSetIdSpec( + scopeId: String, + sourceSetName: String, + ): DokkaSourceSetIdSpec = newInstance<DokkaSourceSetIdSpec>(scopeId, sourceSetName) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetSpec.kt new file mode 100644 index 00000000..9481885b --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/DokkaSourceSetSpec.kt @@ -0,0 +1,366 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetIdSpec.Companion.dokkaSourceSetIdSpec +import org.jetbrains.dokka.dokkatoo.internal.* +import java.io.Serializable +import javax.inject.Inject +import org.gradle.api.* +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.* +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.* + +/** + * [Source set](https://kotlinlang.org/docs/multiplatform-discover-project.html#source-sets) level configuration. + * + * Can be configured in the following way with Gradle Kotlin DSL: + * + * ```kotlin + * // build.gradle.kts + * + * dokkatoo { + * dokkatooSourceSets { + * // configure individual source set by name + * named("customSourceSet") { + * suppress.set(true) + * } + * + * // configure all source sets at once + * configureEach { + * reportUndocumented.set(true) + * } + * } + * } + * ``` + */ +abstract class DokkaSourceSetSpec +@DokkatooInternalApi +@Inject +constructor( + private val name: String, + private val objects: ObjectFactory, +) : + HasConfigurableVisibilityModifiers, + Named, + Serializable, + ExtensionAware { + + @Internal // will be tracked by sourceSetId + override fun getName(): String = name + + /** + * An arbitrary string used to group source sets that originate from different Gradle subprojects. + * This is primarily used by Kotlin Multiplatform projects, which can have multiple source sets + * per subproject. + * + * The default is set from [DokkatooExtension.sourceSetScopeDefault][org.jetbrains.dokka.dokkatoo.DokkatooExtension.sourceSetScopeDefault] + * + * It's unlikely that this value needs to be changed. + */ + @get:Internal // will be tracked by sourceSetId + abstract val sourceSetScope: Property<String> + + /** + * The identifier for this source set, across all Gradle subprojects. + * + * @see sourceSetScope + * @see getName + */ + @get:Input + val sourceSetId: Provider<DokkaSourceSetIdSpec> + get() = sourceSetScope.map { scope -> objects.dokkaSourceSetIdSpec(scope, getName()) } + + /** + * Whether this source set should be skipped when generating documentation. + * + * Default is `false`. + */ + @get:Input + abstract val suppress: Property<Boolean> + + /** + * Display name used to refer to the source set. + * + * The name will be used both externally (for example, source set name visible to documentation readers) and + * internally (for example, for logging messages of [reportUndocumented]). + * + * By default, the value is deduced from information provided by the Kotlin Gradle plugin. + */ + @get:Input + abstract val displayName: Property<String> + + /** + * List of Markdown files that contain + * [module and package documentation](https://kotlinlang.org/docs/reference/dokka-module-and-package-docs.html). + * + * Contents of specified files will be parsed and embedded into documentation as module and package descriptions. + * + * Example of such a file: + * + * ```markdown + * # Module kotlin-demo + * + * The module shows the Dokka usage. + * + * # Package org.jetbrains.kotlin.demo + * + * Contains assorted useful stuff. + * + * ## Level 2 heading + * + * Text after this heading is also part of documentation for `org.jetbrains.kotlin.demo` + * + * # Package org.jetbrains.kotlin.demo2 + * + * Useful stuff in another package. + * ``` + */ + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val includes: ConfigurableFileCollection + + /** + * Set of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations, + * as well as if you want to exclude public declarations and only document internal API. + * + * Can be configured on per-package basis, see [DokkaPackageOptionsSpec.documentedVisibilities]. + * + * Default is [VisibilityModifier.PUBLIC]. + */ + @get:Input + abstract override val documentedVisibilities: SetProperty<VisibilityModifier> + + /** + * Specifies source sets that current source set depends on. + * + * Among other things, this information is needed to resolve + * [expect/actual](https://kotlinlang.org/docs/multiplatform-connect-to-apis.html) declarations. + * + * By default, the values are deduced from information provided by the Kotlin Gradle plugin. + */ + @get:Nested + val dependentSourceSets: NamedDomainObjectContainer<DokkaSourceSetIdSpec> = + extensions.adding("dependentSourceSets", objects.domainObjectContainer()) + + /** + * Classpath for analysis and interactive samples. + * + * Useful if some types that come from dependencies are not resolved/picked up automatically. + * Property accepts both `.jar` and `.klib` files. + * + * By default, classpath is deduced from information provided by the Kotlin Gradle plugin. + */ + @get:Classpath + @get:Optional + abstract val classpath: ConfigurableFileCollection + + /** + * Source code roots to be analyzed and documented. + * Accepts directories and individual `.kt` / `.java` files. + * + * By default, source roots are deduced from information provided by the Kotlin Gradle plugin. + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceRoots: ConfigurableFileCollection + + /** + * List of directories or files that contain sample functions which are referenced via + * [`@sample`](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) KDoc tag. + */ + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val samples: ConfigurableFileCollection + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations without KDocs + * after they have been filtered by [documentedVisibilities]. + * + * Can be overridden for a specific package by setting [DokkaPackageOptionsSpec.reportUndocumented]. + * + * Default is `false`. + */ + @get:Input + abstract val reportUndocumented: Property<Boolean> + + /** + * Specifies the location of the project source code on the Web. If provided, Dokka generates + * "source" links for each declaration. See [DokkaSourceLinkSpec] for more details. + * + * Prefer using [sourceLink] action/closure for adding source links. + * + * @see sourceLink + */ + @get:Nested + abstract val sourceLinks: DomainObjectSet<DokkaSourceLinkSpec> + + /** + * Allows to customize documentation generation options on a per-package basis. + * + * @see DokkaPackageOptionsSpec for details + */ + @get:Nested + abstract val perPackageOptions: DomainObjectSet<DokkaPackageOptionsSpec> + + /** + * Allows linking to Dokka/Javadoc documentation of the project's dependencies. + */ + @get:Nested + val externalDocumentationLinks: NamedDomainObjectContainer<DokkaExternalDocumentationLinkSpec> = + extensions.adding("externalDocumentationLinks", objects.domainObjectContainer()) + + /** + * Platform to be used for setting up code analysis and samples. + * + * The default value is deduced from information provided by the Kotlin Gradle plugin. + */ + @get:Input + abstract val analysisPlatform: Property<KotlinPlatform> + + /** + * Whether to skip packages that contain no visible declarations after + * various filters have been applied. + * + * For instance, if [skipDeprecated] is set to `true` and your package contains only + * deprecated declarations, it will be considered to be empty. + * + * Default is `true`. + */ + @get:Input + abstract val skipEmptyPackages: Property<Boolean> + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be overridden on package level by setting [DokkaPackageOptionsSpec.skipDeprecated]. + * + * Default is `false`. + */ + @get:Input + abstract val skipDeprecated: Property<Boolean> + + /** + * Directories or individual files that should be suppressed, meaning declarations from them + * will be not documented. + * + * Will be concatenated with generated files if [suppressGeneratedFiles] is set to `false`. + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val suppressedFiles: ConfigurableFileCollection + + /** + * Whether to document/analyze generated files. + * + * Generated files are expected to be present under `{project}/{buildDir}/generated` directory. + * If set to `true`, it effectively adds all files from that directory to [suppressedFiles], so + * you can configure it manually. + * + * Default is `true`. + */ + @get:Input + abstract val suppressGeneratedFiles: Property<Boolean> + + /** + * Whether to generate external documentation links that lead to API reference documentation for + * Kotlin's standard library when declarations from it are used. + * + * Default is `true`, meaning links will be generated. + * + * @see externalDocumentationLinks + */ + @get:Input + abstract val enableKotlinStdLibDocumentationLink: Property<Boolean> + + /** + * Whether to generate external documentation links to JDK's Javadocs when declarations from it + * are used. + * + * The version of JDK Javadocs is determined by [jdkVersion] property. + * + * Default is `true`, meaning links will be generated. + * + * @see externalDocumentationLinks + */ + @get:Input + abstract val enableJdkDocumentationLink: Property<Boolean> + + /** + * Whether to generate external documentation links for Android SDK API reference when + * declarations from it are used. + * + * Only relevant in Android projects, ignored otherwise. + * + * Default is `false`, meaning links will not be generated. + * + * @see externalDocumentationLinks + */ + @get:Input + abstract val enableAndroidDocumentationLink: Property<Boolean> + + /** + * [Kotlin language version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [`@sample`](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, the latest language version available to Dokka's embedded compiler will be used. + */ + @get:Input + @get:Optional + abstract val languageVersion: Property<String?> + + /** + * [Kotlin API version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [`@sample`](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, it will be deduced from [languageVersion]. + */ + @get:Input + @get:Optional + abstract val apiVersion: Property<String?> + + /** + * JDK version to use when generating external documentation links for Java types. + * + * For instance, if you use [java.util.UUID] from JDK in some public declaration signature, + * and this property is set to `8`, Dokka will generate an external documentation link + * to [JDK 8 Javadocs](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) for it. + * + * Default is JDK 8. + */ + @get:Input + abstract val jdkVersion: Property<Int> + + /** + * Configure and add a new source link to [sourceLinks]. + * + * @see DokkaSourceLinkSpec + */ + fun sourceLink(action: Action<in DokkaSourceLinkSpec>) { + sourceLinks.add( + objects.newInstance(DokkaSourceLinkSpec::class).also { + action.execute(it) + } + ) + } + + /** + * Action for configuring package options, appending to [perPackageOptions]. + * + * @see DokkaPackageOptionsSpec + */ + fun perPackageOption(action: Action<in DokkaPackageOptionsSpec>) { + perPackageOptions.add( + objects.newInstance(DokkaPackageOptionsSpec::class).also { + action.execute(it) + } + ) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/HasConfigurableVisibilityModifiers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/HasConfigurableVisibilityModifiers.kt new file mode 100644 index 00000000..2ed5ddd9 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/HasConfigurableVisibilityModifiers.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input + +internal interface HasConfigurableVisibilityModifiers { + + @get:Input + val documentedVisibilities: SetProperty<VisibilityModifier> + + /** Sets [documentedVisibilities] (overrides any previously set values). */ + fun documentedVisibilities(vararg visibilities: VisibilityModifier): Unit = + documentedVisibilities.set(visibilities.asList()) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt new file mode 100644 index 00000000..c950fbbe --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/KotlinPlatform.kt @@ -0,0 +1,54 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.Platform + + +/** + * The Kotlin + * + * @see org.jetbrains.dokka.Platform + * @param[displayName] The display name, eventually used in the rendered Dokka publication. + */ +enum class KotlinPlatform( + internal val displayName: String +) { + AndroidJVM("androidJvm"), + Common("common"), + JS("js"), + JVM("jvm"), + Native("native"), + WASM("wasm"), + ; + + companion object { + internal val values: Set<KotlinPlatform> = values().toSet() + + val DEFAULT: KotlinPlatform = JVM + + fun fromString(key: String): KotlinPlatform { + val keyMatch = values.firstOrNull { + it.name.equals(key, ignoreCase = true) || it.displayName.equals(key, ignoreCase = true) + } + if (keyMatch != null) { + return keyMatch + } + + return when (key.lowercase()) { + "android" -> AndroidJVM + "metadata" -> Common + else -> error("Unrecognized platform: $key") + } + } + + // Not defined as a property to try and minimize the dependency on Dokka Core types + internal val KotlinPlatform.dokkaType: Platform + get() = + when (this) { + AndroidJVM, JVM -> Platform.jvm + JS -> Platform.js + WASM -> Platform.wasm + Native -> Platform.native + Common -> Platform.common + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/VisibilityModifier.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/VisibilityModifier.kt new file mode 100644 index 00000000..de61f97b --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/VisibilityModifier.kt @@ -0,0 +1,42 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.DokkaConfiguration + +/** + * Denotes the + * [visibility modifier](https://kotlinlang.org/docs/visibility-modifiers.html) + * of a source code elements. + * + * @see org.jetbrains.dokka.DokkaConfiguration.Visibility + */ +enum class VisibilityModifier { + /** `public` modifier for Java, default visibility for Kotlin */ + PUBLIC, + + /** `private` modifier for both Kotlin and Java */ + PRIVATE, + + /** `protected` modifier for both Kotlin and Java */ + PROTECTED, + + /** Kotlin-specific `internal` modifier */ + INTERNAL, + + /** Java-specific package-private visibility (no modifier) */ + PACKAGE, + ; + + companion object { + internal val entries: Set<VisibilityModifier> = values().toSet() + + // Not defined as a property to try and minimize the dependency on Dokka Core types + internal val VisibilityModifier.dokkaType: DokkaConfiguration.Visibility + get() = when (this) { + PUBLIC -> DokkaConfiguration.Visibility.PUBLIC + PRIVATE -> DokkaConfiguration.Visibility.PRIVATE + PROTECTED -> DokkaConfiguration.Visibility.PROTECTED + INTERNAL -> DokkaConfiguration.Visibility.INTERNAL + PACKAGE -> DokkaConfiguration.Visibility.PACKAGE + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilder.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilder.kt new file mode 100644 index 00000000..c6ff8891 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilder.kt @@ -0,0 +1,33 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaModuleDescriptionSpec +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.File +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.DokkaSourceSetImpl + +/** + * Convert the Gradle-focused [DokkaModuleDescriptionSpec] into a [DokkaSourceSetImpl] instance, + * which will be passed to Dokka Generator. + * + * The conversion is defined in a separate class to try and prevent classes from Dokka Generator + * leaking into the public API. + */ +// to be used to fix https://github.com/adamko-dev/dokkatoo/issues/67 +@DokkatooInternalApi +internal object DokkaModuleDescriptionBuilder { + + fun build( + spec: DokkaModuleDescriptionSpec, + includes: Set<File>, + sourceOutputDirectory: File, + ): DokkaModuleDescriptionImpl = + DokkaModuleDescriptionImpl( + name = spec.name, + relativePathToOutputDirectory = File( + spec.projectPath.get().removePrefix(":").replace(':', '/') + ), + includes = includes, + sourceOutputDirectory = sourceOutputDirectory, + ) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaParametersBuilder.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaParametersBuilder.kt new file mode 100644 index 00000000..d39969a2 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaParametersBuilder.kt @@ -0,0 +1,77 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaGeneratorParametersSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaModuleDescriptionKxs +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaPluginParametersBaseSpec +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.mapNotNullToSet +import java.io.File +import org.gradle.api.logging.Logging +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaSourceSetImpl +import org.jetbrains.dokka.PluginConfigurationImpl + +/** + * Convert the Gradle-focused [DokkaGeneratorParametersSpec] into a [DokkaSourceSetImpl] instance, + * which will be passed to Dokka Generator. + * + * The conversion is defined in a separate class to try and prevent classes from Dokka Generator + * leaking into the public API. + */ +@DokkatooInternalApi +internal object DokkaParametersBuilder { + + fun build( + spec: DokkaGeneratorParametersSpec, + delayTemplateSubstitution: Boolean, + modules: List<DokkaModuleDescriptionKxs>, + outputDirectory: File, + cacheDirectory: File? = null, + ): DokkaConfiguration { + val moduleName = spec.moduleName.get() + val moduleVersion = spec.moduleVersion.orNull?.takeIf { it != "unspecified" } + val offlineMode = spec.offlineMode.get() + val sourceSets = DokkaSourceSetBuilder.buildAll(spec.dokkaSourceSets) + val failOnWarning = spec.failOnWarning.get() + val suppressObviousFunctions = spec.suppressObviousFunctions.get() + val suppressInheritedMembers = spec.suppressInheritedMembers.get() + val finalizeCoroutines = spec.finalizeCoroutines.get() + val pluginsConfiguration = spec.pluginsConfiguration.toSet() + + val pluginsClasspath = spec.pluginsClasspath.files.toList() + val includes = spec.includes.files + + return DokkaConfigurationImpl( + moduleName = moduleName, + moduleVersion = moduleVersion, + outputDir = outputDirectory, + cacheRoot = cacheDirectory, + offlineMode = offlineMode, + sourceSets = sourceSets, + pluginsClasspath = pluginsClasspath, + pluginsConfiguration = pluginsConfiguration.map(::build), + modules = modules.map(DokkaModuleDescriptionKxs::convert), +// modules = modules.map { +// it.convert( +// moduleDescriptionFiles.get(it.name) +// ?: error("missing module description files for ${it.name}") +// ) +// }, + failOnWarning = failOnWarning, + delayTemplateSubstitution = delayTemplateSubstitution, + suppressObviousFunctions = suppressObviousFunctions, + includes = includes, + suppressInheritedMembers = suppressInheritedMembers, + finalizeCoroutines = finalizeCoroutines, + ) + } + + private fun build(spec: DokkaPluginParametersBaseSpec): PluginConfigurationImpl { + return PluginConfigurationImpl( + fqPluginName = spec.pluginFqn, + serializationFormat = DokkaConfiguration.SerializationFormat.JSON, + values = spec.jsonEncode(), + ) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaSourceSetBuilder.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaSourceSetBuilder.kt new file mode 100644 index 00000000..77935d8c --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/parameters/builders/DokkaSourceSetBuilder.kt @@ -0,0 +1,112 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.* +import org.jetbrains.dokka.dokkatoo.dokka.parameters.KotlinPlatform.Companion.dokkaType +import org.jetbrains.dokka.dokkatoo.dokka.parameters.VisibilityModifier.Companion.dokkaType +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.mapNotNullToSet +import org.jetbrains.dokka.dokkatoo.internal.mapToSet +import org.gradle.api.logging.Logging +import org.jetbrains.dokka.* + + +/** + * Convert the Gradle-focused [DokkaSourceSetSpec] into a [DokkaSourceSetImpl] instance, which + * will be passed to Dokka Generator. + * + * The conversion is defined in a separate class to try and prevent classes from Dokka Generator + * leaking into the public API. + */ +@DokkatooInternalApi +internal object DokkaSourceSetBuilder { + + private val logger = Logging.getLogger(DokkaParametersBuilder::class.java) + + fun buildAll(sourceSets: Set<DokkaSourceSetSpec>): List<DokkaSourceSetImpl> { + + val suppressedSourceSetIds = sourceSets.mapNotNullToSet { + val suppressed = it.suppress.get() + val sourceSetId = it.sourceSetId.get() + if (suppressed) { + logger.info("Dokka source set $sourceSetId is suppressed") + sourceSetId + } else { + logger.info("Dokka source set $sourceSetId isn't suppressed") + null + } + } + + val enabledSourceSets = sourceSets.filter { it.sourceSetId.get() !in suppressedSourceSetIds } + + return enabledSourceSets.map { build(it, suppressedSourceSetIds) } + } + + private fun build( + spec: DokkaSourceSetSpec, + suppressedSourceSetIds: Set<DokkaSourceSetIdSpec>, + ): DokkaSourceSetImpl { + + val dependentSourceSets = + (spec.dependentSourceSets subtract suppressedSourceSetIds).mapToSet(::build) + + return DokkaSourceSetImpl( + // properties + analysisPlatform = spec.analysisPlatform.get().dokkaType, + apiVersion = spec.apiVersion.orNull, + dependentSourceSets = dependentSourceSets, + displayName = spec.displayName.get(), + documentedVisibilities = spec.documentedVisibilities.get().mapToSet { it.dokkaType }, + externalDocumentationLinks = spec.externalDocumentationLinks.mapNotNullToSet(::build), + jdkVersion = spec.jdkVersion.get(), + languageVersion = spec.languageVersion.orNull, + noJdkLink = !spec.enableJdkDocumentationLink.get(), + noStdlibLink = !spec.enableKotlinStdLibDocumentationLink.get(), + perPackageOptions = spec.perPackageOptions.map(::build), + reportUndocumented = spec.reportUndocumented.get(), + skipDeprecated = spec.skipDeprecated.get(), + skipEmptyPackages = spec.skipEmptyPackages.get(), + sourceLinks = spec.sourceLinks.mapToSet { build(it) }, + sourceSetID = build(spec.sourceSetId.get()), + + // files + classpath = spec.classpath.files.toList(), + includes = spec.includes.files, + samples = spec.samples.files, + sourceRoots = spec.sourceRoots.files, + suppressedFiles = spec.suppressedFiles.files, + ) + } + + private fun build(spec: DokkaExternalDocumentationLinkSpec): ExternalDocumentationLinkImpl? { + if (!spec.enabled.getOrElse(true)) return null + + return ExternalDocumentationLinkImpl( + url = spec.url.get().toURL(), + packageListUrl = spec.packageListUrl.get().toURL(), + ) + } + + private fun build(spec: DokkaPackageOptionsSpec): PackageOptionsImpl = + PackageOptionsImpl( + matchingRegex = spec.matchingRegex.get(), + documentedVisibilities = spec.documentedVisibilities.get().mapToSet { it.dokkaType }, + reportUndocumented = spec.reportUndocumented.get(), + skipDeprecated = spec.skipDeprecated.get(), + suppress = spec.suppress.get(), + includeNonPublic = DokkaDefaults.includeNonPublic, + ) + + private fun build(spec: DokkaSourceSetIdSpec): DokkaSourceSetID = + DokkaSourceSetID( + scopeId = spec.scopeId, + sourceSetName = spec.sourceSetName + ) + + private fun build(spec: DokkaSourceLinkSpec): SourceLinkDefinitionImpl = + SourceLinkDefinitionImpl( + localDirectory = spec.localDirectory.asFile.get().invariantSeparatorsPath, + remoteUrl = spec.remoteUrl.get().toURL(), + remoteLineSuffix = spec.remoteLineSuffix.orNull, + ) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaHtmlPluginParameters.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaHtmlPluginParameters.kt new file mode 100644 index 00000000..a3252b51 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaHtmlPluginParameters.kt @@ -0,0 +1,129 @@ +package org.jetbrains.dokka.dokkatoo.dokka.plugins + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.addAll +import org.jetbrains.dokka.dokkatoo.internal.putIfNotNull +import javax.inject.Inject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.putJsonArray +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE + + +/** + * Configuration for Dokka's base HTML format + * + * [More information is available in the Dokka docs.](https://kotlinlang.org/docs/dokka-html.html#configuration) + */ +abstract class DokkaHtmlPluginParameters +@DokkatooInternalApi +@Inject +constructor( + name: String +) : DokkaPluginParametersBaseSpec( + name, + DOKKA_HTML_PLUGIN_FQN, +) { + + /** + * List of paths for image assets to be bundled with documentation. + * The image assets can have any file extension. + * + * For more information, see + * [Customizing assets](https://kotlinlang.org/docs/dokka-html.html#customize-assets). + * + * Be aware that files will be copied as-is to a specific directory inside the assembled Dokka + * publication. This means that any relative paths must be written in such a way that they will + * work _after_ the files are moved into the publication. + * + * It's best to try and mirror Dokka's directory structure in the source files, which can help + * IDE inspections. + */ + @get:InputFiles + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val customAssets: ConfigurableFileCollection + + /** + * List of paths for `.css` stylesheets to be bundled with documentation and used for rendering. + * + * For more information, see + * [Customizing assets](https://kotlinlang.org/docs/dokka-html.html#customize-assets). + * + * Be aware that files will be copied as-is to a specific directory inside the assembled Dokka + * publication. This means that any relative paths must be written in such a way that they will + * work _after_ the files are moved into the publication. + * + * It's best to try and mirror Dokka's directory structure in the source files, which can help + * IDE inspections. + */ + @get:InputFiles + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val customStyleSheets: ConfigurableFileCollection + + /** + * This is a boolean option. If set to `true`, Dokka renders properties/functions and inherited + * properties/inherited functions separately. + * + * This is disabled by default. + */ + @get:Input + @get:Optional + abstract val separateInheritedMembers: Property<Boolean> + + /** + * This is a boolean option. If set to `true`, Dokka merges declarations that are not declared as + * [expect/actual](https://kotlinlang.org/docs/multiplatform-connect-to-apis.html), but have the + * same fully qualified name. This can be useful for legacy codebases. + * + * This is disabled by default. + */ + @get:Input + @get:Optional + abstract val mergeImplicitExpectActualDeclarations: Property<Boolean> + + /** The text displayed in the footer. */ + @get:Input + @get:Optional + abstract val footerMessage: Property<String> + + /** + * Path to the directory containing custom HTML templates. + * + * For more information, see [Templates](https://kotlinlang.org/docs/dokka-html.html#templates). + */ + @get:InputDirectory + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val templatesDir: DirectoryProperty + + override fun jsonEncode(): String = + buildJsonObject { + putJsonArray("customAssets") { + addAll(customAssets.files) + } + putJsonArray("customStyleSheets") { + addAll(customStyleSheets.files) + } + putIfNotNull("separateInheritedMembers", separateInheritedMembers.orNull) + putIfNotNull( + "mergeImplicitExpectActualDeclarations", + mergeImplicitExpectActualDeclarations.orNull + ) + putIfNotNull("footerMessage", footerMessage.orNull) + putIfNotNull("footerMessage", footerMessage.orNull) + putIfNotNull( + "templatesDir", + templatesDir.orNull?.asFile?.canonicalFile?.invariantSeparatorsPath + ) + }.toString() + + companion object { + const val DOKKA_HTML_PARAMETERS_NAME = "html" + const val DOKKA_HTML_PLUGIN_FQN = "org.jetbrains.dokka.base.DokkaBase" + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBaseSpec.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBaseSpec.kt new file mode 100644 index 00000000..486bb80e --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBaseSpec.kt @@ -0,0 +1,32 @@ +package org.jetbrains.dokka.dokkatoo.dokka.plugins + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.Serializable +import javax.inject.Inject +import org.gradle.api.Named +import org.gradle.api.tasks.Input + +/** + * Base class for defining Dokka Plugin configuration. + * + * This class should not be instantiated directly. Instead, use a subclass, or create plugin + * parameters dynamically using [DokkaPluginParametersBuilder]. + * + * [More information about Dokka Plugins is available in the Dokka docs.](https://kotlinlang.org/docs/dokka-plugins.html) + * + * @param[pluginFqn] Fully qualified classname of the Dokka Plugin + */ +abstract class DokkaPluginParametersBaseSpec +@DokkatooInternalApi +@Inject +constructor( + private val name: String, + @get:Input + open val pluginFqn: String, +) : Serializable, Named { + + abstract fun jsonEncode(): String // to be implemented by subclasses + + @Input + override fun getName(): String = name +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBuilder.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBuilder.kt new file mode 100644 index 00000000..a29b94c2 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaPluginParametersBuilder.kt @@ -0,0 +1,232 @@ +package org.jetbrains.dokka.dokkatoo.dokka.plugins + +import org.jetbrains.dokka.dokkatoo.internal.DokkaPluginParametersContainer +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import java.io.File +import javax.inject.Inject +import kotlinx.serialization.json.* +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE +import org.gradle.kotlin.dsl.* + + +/** + * Dynamically create some configuration to control the behaviour of a Dokka Plugin. + * + * @param[pluginFqn] The fully-qualified name of a Dokka Plugin. For example, the FQN of the + * [Dokka Base plugin](https://github.com/Kotlin/dokka/tree/master/plugins/base#readme) + * is `org.jetbrains.dokka.base.DokkaBase` + */ +fun DokkaPluginParametersContainer.pluginParameters( + pluginFqn: String, + configure: DokkaPluginParametersBuilder.() -> Unit +) { + containerWithType(DokkaPluginParametersBuilder::class) + .maybeCreate(pluginFqn) + .configure() +} + + +/** + * Dynamically create some configuration to control the behaviour of a Dokka Plugin. + * + * This type of builder is necessary to respect + * [Gradle incremental build annotations](https://docs.gradle.org/current/userguide/incremental_build.html#sec:task_input_output_annotations). + * + * @param[pluginFqn] The fully-qualified name of a Dokka Plugin. For example, the Dokka Base plugin's FQN is `org.jetbrains.dokka.base.DokkaBase` + */ +abstract class DokkaPluginParametersBuilder +@Inject +@DokkatooInternalApi +constructor( + name: String, + @get:Input + override val pluginFqn: String, + + @Internal + internal val objects: ObjectFactory, +) : DokkaPluginParametersBaseSpec(name, pluginFqn) { + + @get:Nested + internal val properties = PluginConfigValue.Properties(objects.mapProperty()) + + @Internal + override fun jsonEncode(): String = properties.convertToJson().toString() + + companion object { + private fun PluginConfigValue.convertToJson(): JsonElement = + when (this) { + is PluginConfigValue.DirectoryValue -> directory.asFile.orNull.convertToJson() + is PluginConfigValue.FileValue -> file.asFile.orNull.convertToJson() + is PluginConfigValue.FilesValue -> JsonArray(files.files.map { it.convertToJson() }) + + is PluginConfigValue.BooleanValue -> JsonPrimitive(boolean) + is PluginConfigValue.NumberValue -> JsonPrimitive(number) + is PluginConfigValue.StringValue -> JsonPrimitive(string) + + is PluginConfigValue.Properties -> + JsonObject(values.get().mapValues { (_, value) -> value.convertToJson() }) + + is PluginConfigValue.Values -> + JsonArray(values.get().map { it.convertToJson() }) + } + + /** Creates a [JsonPrimitive] from the given [File]. */ + private fun File?.convertToJson(): JsonPrimitive = + JsonPrimitive(this?.canonicalFile?.invariantSeparatorsPath) + } +} + + +fun DokkaPluginParametersBuilder.files( + propertyName: String, + filesConfig: ConfigurableFileCollection.() -> Unit +) { + val files = objects.fileCollection() + files.filesConfig() + properties.values.put(propertyName, PluginConfigValue.FilesValue(files)) +} + +//region Primitive Properties +fun DokkaPluginParametersBuilder.property(propertyName: String, value: String) { + properties.values.put(propertyName, PluginConfigValue(value)) +} + +fun DokkaPluginParametersBuilder.property(propertyName: String, value: Number) { + properties.values.put(propertyName, PluginConfigValue(value)) +} + +fun DokkaPluginParametersBuilder.property(propertyName: String, value: Boolean) { + properties.values.put(propertyName, PluginConfigValue(value)) +} + +@JvmName("stringProperty") +fun DokkaPluginParametersBuilder.property(propertyName: String, provider: Provider<String>) { + properties.values.put(propertyName, provider.map { PluginConfigValue(it) }) +} + +@JvmName("numberProperty") +fun DokkaPluginParametersBuilder.property(propertyName: String, provider: Provider<Number>) { + properties.values.put(propertyName, provider.map { PluginConfigValue(it) }) +} + +@JvmName("booleanProperty") +fun DokkaPluginParametersBuilder.property( + propertyName: String, + provider: Provider<Boolean> +) { + properties.values.put(propertyName, provider.map { PluginConfigValue(it) }) +} +//endregion + + +//region List Properties +fun DokkaPluginParametersBuilder.properties( + propertyName: String, + build: PluginConfigValue.Values.() -> Unit +) { + val values = PluginConfigValue.Values(objects.listProperty()) + values.build() + properties.values.put(propertyName, values) +} + +fun PluginConfigValue.Values.add(value: String) = + values.add(PluginConfigValue(value)) + +fun PluginConfigValue.Values.add(value: Number) = + values.add(PluginConfigValue(value)) + +fun PluginConfigValue.Values.add(value: Boolean) = + values.add(PluginConfigValue(value)) + +@JvmName("addString") +fun PluginConfigValue.Values.add(value: Provider<String>) = + values.add(PluginConfigValue(value)) + +@JvmName("addNumber") +fun PluginConfigValue.Values.add(value: Provider<Number>) = + values.add(PluginConfigValue(value)) + +@JvmName("addBoolean") +fun PluginConfigValue.Values.add(value: Provider<Boolean>) = + values.add(PluginConfigValue(value)) +//endregion + + +sealed interface PluginConfigValue { + + /** An input file */ + class FileValue( + @InputFile + @PathSensitive(RELATIVE) + val file: RegularFileProperty, + ) : PluginConfigValue + + /** Input files and directories */ + class FilesValue( + @InputFiles + @PathSensitive(RELATIVE) + val files: ConfigurableFileCollection, + ) : PluginConfigValue + + /** An input directory */ + class DirectoryValue( + @InputDirectory + @PathSensitive(RELATIVE) + val directory: DirectoryProperty, + ) : PluginConfigValue + + /** Key-value properties. Analogous to a [JsonObject]. */ + class Properties( + @Nested + val values: MapProperty<String, PluginConfigValue> + ) : PluginConfigValue + + /** Multiple values. Analogous to a [JsonArray]. */ + class Values( + @Nested + val values: ListProperty<PluginConfigValue> + ) : PluginConfigValue + + sealed interface Primitive : PluginConfigValue + + /** A basic [String] value */ + class StringValue(@Input val string: String) : Primitive + + /** A basic [Number] value */ + class NumberValue(@Input val number: Number) : Primitive + + /** A basic [Boolean] value */ + class BooleanValue(@Input val boolean: Boolean) : Primitive +} + +fun PluginConfigValue(value: String) = + PluginConfigValue.StringValue(value) + +fun PluginConfigValue(value: Number) = + PluginConfigValue.NumberValue(value) + +fun PluginConfigValue(value: Boolean) = + PluginConfigValue.BooleanValue(value) + +@Suppress("FunctionName") +@JvmName("PluginConfigStringValue") +fun PluginConfigValue(value: Provider<String>): Provider<PluginConfigValue.StringValue> = + value.map { PluginConfigValue(it) } + +@Suppress("FunctionName") +@JvmName("PluginConfigNumberValue") +fun PluginConfigValue(value: Provider<Number>): Provider<PluginConfigValue.NumberValue> = + value.map { PluginConfigValue(it) } + +@Suppress("FunctionName") +@JvmName("PluginConfigBooleanValue") +fun PluginConfigValue(value: Provider<Boolean>): Provider<PluginConfigValue.BooleanValue> = + value.map { PluginConfigValue(it) } diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaVersioningPluginParameters.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaVersioningPluginParameters.kt new file mode 100644 index 00000000..1a4d75f2 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/dokka/plugins/DokkaVersioningPluginParameters.kt @@ -0,0 +1,101 @@ +package org.jetbrains.dokka.dokkatoo.dokka.plugins + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.addAll +import org.jetbrains.dokka.dokkatoo.internal.addAllIfNotNull +import org.jetbrains.dokka.dokkatoo.internal.putIfNotNull +import javax.inject.Inject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.putJsonArray +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE + + +/** + * Configuration for + * [Dokka's Versioning plugin](https://github.com/Kotlin/dokka/tree/master/plugins/versioning#readme). + * + * The versioning plugin provides the ability to host documentation for multiple versions of your + * library/application with seamless switching between them. This, in turn, provides a better + * experience for your users. + * + * Note: The versioning plugin only works with Dokka's HTML format. + */ +abstract class DokkaVersioningPluginParameters +@DokkatooInternalApi +@Inject +constructor( + name: String, +) : DokkaPluginParametersBaseSpec( + name, + DOKKA_VERSIONING_PLUGIN_FQN, +) { + + /** + * The version of your application/library that documentation is going to be generated for. + * This will be the version shown in the dropdown menu. + */ + @get:Input + @get:Optional + abstract val version: Property<String> + + /** + * An optional list of strings that represents the order that versions should appear in the + * dropdown menu. + * + * Must match [version] string exactly. The first item in the list is at the top of the dropdown. + */ + @get:Input + @get:Optional + abstract val versionsOrdering: ListProperty<String> + + /** + * An optional path to a parent folder that contains other documentation versions. + * It requires a specific directory structure. + * + * For more information, see + * [Directory structure](https://github.com/Kotlin/dokka/blob/master/plugins/versioning/README.md#directory-structure). + */ + @get:InputDirectory + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val olderVersionsDir: DirectoryProperty + + /** + * An optional list of paths to other documentation versions. It must point to Dokka's outputs + * directly. This is useful if different versions can't all be in the same directory. + */ + @get:InputFiles + @get:PathSensitive(RELATIVE) + @get:Optional + abstract val olderVersions: ConfigurableFileCollection + + /** + * An optional boolean value indicating whether to render the navigation dropdown on all pages. + * + * Set to `true` by default. + */ + @get:Input + @get:Optional + abstract val renderVersionsNavigationOnAllPages: Property<Boolean> + + override fun jsonEncode(): String = + buildJsonObject { + putIfNotNull("version", version.orNull) + putJsonArray("versionsOrdering") { addAllIfNotNull(versionsOrdering.orNull) } + putIfNotNull("olderVersionsDir", olderVersionsDir.orNull?.asFile) + putJsonArray("olderVersions") { + addAll(olderVersions.files) + } + putIfNotNull("renderVersionsNavigationOnAllPages", renderVersionsNavigationOnAllPages.orNull) + }.toString() + + companion object { + const val DOKKA_VERSIONING_PLUGIN_PARAMETERS_NAME = "versioning" + const val DOKKA_VERSIONING_PLUGIN_FQN = "org.jetbrains.dokka.versioning.VersioningPlugin" + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatDependencyContainers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatDependencyContainers.kt new file mode 100644 index 00000000..08eece77 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatDependencyContainers.kt @@ -0,0 +1,152 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKATOO_BASE_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKATOO_CATEGORY_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.distributions.DokkatooConfigurationAttributes.Companion.DOKKA_FORMAT_ATTRIBUTE +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.asConsumer +import org.jetbrains.dokka.dokkatoo.internal.asProvider +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.AttributeContainer +import org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE +import org.gradle.api.attributes.Bundling.EXTERNAL +import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE +import org.gradle.api.attributes.Category.LIBRARY +import org.gradle.api.attributes.LibraryElements.JAR +import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE +import org.gradle.api.attributes.Usage.JAVA_RUNTIME +import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE +import org.gradle.api.attributes.java.TargetJvmEnvironment.STANDARD_JVM +import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.* + +/** + * The Dokka-specific Gradle [Configuration]s used to produce and consume files from external sources + * (example: Maven Central), or between subprojects. + * + * (Be careful of the confusing names: Gradle [Configuration]s are used to transfer files, + * [DokkaConfiguration][org.jetbrains.dokka.DokkaConfiguration] + * is used to configure Dokka behaviour.) + */ +@DokkatooInternalApi +class DokkatooFormatDependencyContainers( + private val formatName: String, + dokkatooConsumer: NamedDomainObjectProvider<Configuration>, + project: Project, +) { + + private val objects: ObjectFactory = project.objects + + private val dependencyContainerNames = DokkatooBasePlugin.DependencyContainerNames(formatName) + + private val dokkatooAttributes: DokkatooConfigurationAttributes = objects.newInstance() + + private fun AttributeContainer.dokkaCategory(category: DokkatooConfigurationAttributes.DokkatooCategoryAttribute) { + attribute(DOKKATOO_BASE_ATTRIBUTE, dokkatooAttributes.dokkatooBaseUsage) + attribute(DOKKA_FORMAT_ATTRIBUTE, objects.named(formatName)) + attribute(DOKKATOO_CATEGORY_ATTRIBUTE, category) + } + + private fun AttributeContainer.jvmJar() { + attribute(USAGE_ATTRIBUTE, objects.named(JAVA_RUNTIME)) + attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY)) + attribute(BUNDLING_ATTRIBUTE, objects.named(EXTERNAL)) + attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(STANDARD_JVM)) + attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR)) + } + + //<editor-fold desc="Dokka Module files"> + /** Fetch Dokka Module files from other subprojects */ + val dokkaModuleConsumer: NamedDomainObjectProvider<Configuration> = + project.configurations.register(dependencyContainerNames.dokkatooModuleFilesConsumer) { + description = "Fetch Dokka Module files for $formatName from other subprojects" + asConsumer() + extendsFrom(dokkatooConsumer.get()) + attributes { + dokkaCategory(dokkatooAttributes.dokkaModuleFiles) + } + } + /** Provide Dokka Module files to other subprojects */ + val dokkaModuleOutgoing: NamedDomainObjectProvider<Configuration> = + project.configurations.register(dependencyContainerNames.dokkatooModuleFilesProvider) { + description = "Provide Dokka Module files for $formatName to other subprojects" + asProvider() + // extend from dokkaConfigurationsConsumer, so Dokka Module Configs propagate api() style + extendsFrom(dokkaModuleConsumer.get()) + attributes { + dokkaCategory(dokkatooAttributes.dokkaModuleFiles) + } + } + //</editor-fold> + + //<editor-fold desc="Dokka Generator Plugins"> + /** + * Dokka plugins. + * + * Users can add plugins to this dependency. + * + * Should not contain runtime dependencies. + */ + val dokkaPluginsClasspath: NamedDomainObjectProvider<Configuration> = + project.configurations.register(dependencyContainerNames.dokkaPluginsClasspath) { + description = "Dokka Plugins classpath for $formatName" + asConsumer() + attributes { + jvmJar() + dokkaCategory(dokkatooAttributes.dokkaPluginsClasspath) + } + } + + /** + * Dokka Plugins, without transitive dependencies. + * + * It extends [dokkaPluginsClasspath], so do not add dependencies to this configuration - + * the dependencies are computed automatically. + */ + val dokkaPluginsIntransitiveClasspath: NamedDomainObjectProvider<Configuration> = + project.configurations.register(dependencyContainerNames.dokkaPluginsIntransitiveClasspath) { + description = + "Dokka Plugins classpath for $formatName - for internal use. Fetch only the plugins (no transitive dependencies) for use in the Dokka JSON Configuration." + asConsumer() + extendsFrom(dokkaPluginsClasspath.get()) + isTransitive = false + attributes { + jvmJar() + dokkaCategory(dokkatooAttributes.dokkaPluginsClasspath) + } + } + //</editor-fold> + + //<editor-fold desc="Dokka Generator Classpath"> + /** + * Runtime classpath used to execute Dokka Worker. + * + * This configuration is not exposed to other subprojects. + * + * Extends [dokkaPluginsClasspath]. + * + * @see org.jetbrains.dokka.dokkatoo.workers.DokkaGeneratorWorker + * @see org.jetbrains.dokka.dokkatoo.tasks.DokkatooGenerateTask + */ + val dokkaGeneratorClasspath: NamedDomainObjectProvider<Configuration> = + project.configurations.register(dependencyContainerNames.dokkaGeneratorClasspath) { + description = + "Dokka Generator runtime classpath for $formatName - will be used in Dokka Worker. Should contain all transitive dependencies, plugins (and their transitive dependencies), so Dokka Worker can run." + asConsumer() + + // extend from plugins classpath, so Dokka Worker can run the plugins + extendsFrom(dokkaPluginsClasspath.get()) + + isTransitive = true + attributes { + jvmJar() + dokkaCategory(dokkatooAttributes.dokkaGeneratorClasspath) + } + } + //</editor-fold> +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt new file mode 100644 index 00000000..c8f601a6 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatPlugin.kt @@ -0,0 +1,174 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.adapters.DokkatooAndroidAdapter +import org.jetbrains.dokka.dokkatoo.adapters.DokkatooJavaAdapter +import org.jetbrains.dokka.dokkatoo.adapters.DokkatooKotlinAdapter +import org.jetbrains.dokka.dokkatoo.internal.* +import javax.inject.Inject +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.kotlin.dsl.* + +/** + * Base Gradle Plugin for setting up a Dokka Publication for a specific format. + * + * [DokkatooBasePlugin] must be applied for this plugin (or any subclass) to have an effect. + * + * Anyone can use this class as a basis for a generating a Dokka Publication in a custom format. + */ +abstract class DokkatooFormatPlugin( + val formatName: String, +) : Plugin<Project> { + + @get:Inject + @DokkatooInternalApi + protected abstract val objects: ObjectFactory + @get:Inject + @DokkatooInternalApi + protected abstract val providers: ProviderFactory + @get:Inject + @DokkatooInternalApi + protected abstract val files: FileSystemOperations + + + override fun apply(target: Project) { + + // apply DokkatooBasePlugin + target.pluginManager.apply(DokkatooBasePlugin::class) + + // apply the plugin that will autoconfigure Dokkatoo to use the sources of a Kotlin project + target.pluginManager.apply(type = DokkatooKotlinAdapter::class) + target.pluginManager.apply(type = DokkatooJavaAdapter::class) + target.pluginManager.apply(type = DokkatooAndroidAdapter::class) + + target.plugins.withType<DokkatooBasePlugin>().configureEach { + val dokkatooExtension = target.extensions.getByType(DokkatooExtension::class) + + val publication = dokkatooExtension.dokkatooPublications.create(formatName) + + val dokkatooConsumer = + target.configurations.named(DokkatooBasePlugin.dependencyContainerNames.dokkatoo) + + val dependencyContainers = DokkatooFormatDependencyContainers( + formatName = formatName, + dokkatooConsumer = dokkatooConsumer, + project = target, + ) + + val dokkatooTasks = DokkatooFormatTasks( + project = target, + publication = publication, + dokkatooExtension = dokkatooExtension, + dependencyContainers = dependencyContainers, + providers = providers, + ) + + dependencyContainers.dokkaModuleOutgoing.configure { + outgoing { + artifact(dokkatooTasks.prepareModuleDescriptor.flatMap { it.dokkaModuleDescriptorJson }) + } + outgoing { + artifact(dokkatooTasks.generateModule.flatMap { it.outputDirectory }) { + type = "directory" + } + } + } + + // TODO DokkaCollect replacement - share raw files without first generating a Dokka Module + //dependencyCollections.dokkaParametersOutgoing.configure { + // outgoing { + // artifact(dokkatooTasks.prepareParametersTask.flatMap { it.dokkaConfigurationJson }) + // } + //} + + val context = DokkatooFormatPluginContext( + project = target, + dokkatooExtension = dokkatooExtension, + dokkatooTasks = dokkatooTasks, + formatName = formatName, + ) + + context.configure() + + if (context.addDefaultDokkaDependencies) { + with(context) { + addDefaultDokkaDependencies() + } + } + } + } + + + /** Format specific configuration - to be implemented by subclasses */ + open fun DokkatooFormatPluginContext.configure() {} + + + @DokkatooInternalApi + class DokkatooFormatPluginContext( + val project: Project, + val dokkatooExtension: DokkatooExtension, + val dokkatooTasks: DokkatooFormatTasks, + formatName: String, + ) { + private val dependencyContainerNames = DokkatooBasePlugin.DependencyContainerNames(formatName) + + var addDefaultDokkaDependencies = true + + /** Create a [Dependency] for a Dokka module */ + fun DependencyHandler.dokka(module: String): Provider<Dependency> = + dokkatooExtension.versions.jetbrainsDokka.map { version -> create("org.jetbrains.dokka:$module:$version") } + + /** Add a dependency to the Dokka plugins classpath */ + fun DependencyHandler.dokkaPlugin(dependency: Provider<Dependency>): Unit = + addProvider(dependencyContainerNames.dokkaPluginsClasspath, dependency) + + /** Add a dependency to the Dokka plugins classpath */ + fun DependencyHandler.dokkaPlugin(dependency: String) { + add(dependencyContainerNames.dokkaPluginsClasspath, dependency) + } + + /** Add a dependency to the Dokka Generator classpath */ + fun DependencyHandler.dokkaGenerator(dependency: Provider<Dependency>) { + addProvider(dependencyContainerNames.dokkaGeneratorClasspath, dependency) + } + + /** Add a dependency to the Dokka Generator classpath */ + fun DependencyHandler.dokkaGenerator(dependency: String) { + add(dependencyContainerNames.dokkaGeneratorClasspath, dependency) + } + } + + + private fun DokkatooFormatPluginContext.addDefaultDokkaDependencies() { + project.dependencies { + /** lazily create a [Dependency] with the provided [version] */ + infix fun String.version(version: Property<String>): Provider<Dependency> = + version.map { v -> create("$this:$v") } + + with(dokkatooExtension.versions) { + dokkaPlugin(dokka("analysis-kotlin-descriptors")) + dokkaPlugin(dokka("templating-plugin")) + dokkaPlugin(dokka("dokka-base")) +// dokkaPlugin(dokka("all-modules-page-plugin")) + + dokkaPlugin("org.jetbrains.kotlinx:kotlinx-html" version kotlinxHtml) + dokkaPlugin("org.freemarker:freemarker" version freemarker) + + dokkaGenerator(dokka("dokka-core")) + // TODO why does org.jetbrains:markdown need a -jvm suffix? + dokkaGenerator("org.jetbrains:markdown-jvm" version jetbrainsMarkdown) + dokkaGenerator("org.jetbrains.kotlinx:kotlinx-coroutines-core" version kotlinxCoroutines) + } + } + } + +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatTasks.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatTasks.kt new file mode 100644 index 00000000..ab3639bc --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooFormatTasks.kt @@ -0,0 +1,105 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.LocalProjectOnlyFilter +import org.jetbrains.dokka.dokkatoo.internal.configuring +import org.jetbrains.dokka.dokkatoo.tasks.DokkatooGenerateTask +import org.jetbrains.dokka.dokkatoo.tasks.DokkatooPrepareModuleDescriptorTask +import org.gradle.api.Project +import org.gradle.api.provider.ProviderFactory +import org.gradle.kotlin.dsl.* + +/** Tasks for generating a Dokkatoo Publication in a specific format. */ +@DokkatooInternalApi +class DokkatooFormatTasks( + project: Project, + private val publication: DokkaPublication, + private val dokkatooExtension: DokkatooExtension, + private val dependencyContainers: DokkatooFormatDependencyContainers, + + private val providers: ProviderFactory, +) { + private val formatName: String get() = publication.formatName + + private val taskNames = DokkatooBasePlugin.TaskNames(formatName) + + private fun DokkatooGenerateTask.applyFormatSpecificConfiguration() { + runtimeClasspath.from( + dependencyContainers.dokkaGeneratorClasspath.map { classpath -> + classpath.incoming.artifacts.artifactFiles + } + ) + generator.apply { + publicationEnabled.convention(publication.enabled) + + failOnWarning.convention(publication.failOnWarning) + finalizeCoroutines.convention(publication.finalizeCoroutines) + includes.from(publication.includes) + moduleName.convention(publication.moduleName) + moduleVersion.convention(publication.moduleVersion) + offlineMode.convention(publication.offlineMode) + pluginsConfiguration.addAllLater(providers.provider { publication.pluginsConfiguration }) + pluginsClasspath.from( + dependencyContainers.dokkaPluginsIntransitiveClasspath.map { classpath -> + classpath.incoming.artifacts.artifactFiles + } + ) + suppressInheritedMembers.convention(publication.suppressInheritedMembers) + suppressObviousFunctions.convention(publication.suppressObviousFunctions) + } + } + + val generatePublication = project.tasks.register<DokkatooGenerateTask>( + taskNames.generatePublication, + publication.pluginsConfiguration, + ).configuring task@{ + description = "Executes the Dokka Generator, generating the $formatName publication" + generationType.set(DokkatooGenerateTask.GenerationType.PUBLICATION) + + outputDirectory.convention(dokkatooExtension.dokkatooPublicationDirectory.dir(formatName)) + + generator.apply { + // depend on Dokka Module Descriptors from other subprojects + dokkaModuleFiles.from( + dependencyContainers.dokkaModuleConsumer.map { modules -> + modules.incoming + .artifactView { componentFilter(LocalProjectOnlyFilter) } + .artifacts.artifactFiles + } + ) + } + + applyFormatSpecificConfiguration() + } + + val generateModule = project.tasks.register<DokkatooGenerateTask>( + taskNames.generateModule, + publication.pluginsConfiguration, + ).configuring task@{ + description = "Executes the Dokka Generator, generating a $formatName module" + generationType.set(DokkatooGenerateTask.GenerationType.MODULE) + + outputDirectory.convention(dokkatooExtension.dokkatooModuleDirectory.dir(formatName)) + + applyFormatSpecificConfiguration() + } + + val prepareModuleDescriptor = project.tasks.register<DokkatooPrepareModuleDescriptorTask>( + taskNames.prepareModuleDescriptor + ) task@{ + description = "Prepares the Dokka Module Descriptor for $formatName" + includes.from(publication.includes) + dokkaModuleDescriptorJson.convention( + dokkatooExtension.dokkatooConfigurationsDirectory.file("$formatName/module_descriptor.json") + ) + moduleDirectory.set(generateModule.flatMap { it.outputDirectory }) + +// dokkaSourceSets.addAllLater(providers.provider { dokkatooExtension.dokkatooSourceSets }) +// dokkaSourceSets.configureEach { +// sourceSetScope.convention(this@task.path) +// } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooGfmPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooGfmPlugin.kt new file mode 100644 index 00000000..79df47df --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooGfmPlugin.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.gradle.kotlin.dsl.* + +abstract class DokkatooGfmPlugin +@DokkatooInternalApi +constructor() : DokkatooFormatPlugin(formatName = "gfm") { + override fun DokkatooFormatPluginContext.configure() { + project.dependencies { + dokkaPlugin(dokka("gfm-plugin")) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooHtmlPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooHtmlPlugin.kt new file mode 100644 index 00000000..5748f7d1 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooHtmlPlugin.kt @@ -0,0 +1,72 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaHtmlPluginParameters +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaHtmlPluginParameters.Companion.DOKKA_HTML_PARAMETERS_NAME +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaVersioningPluginParameters +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaVersioningPluginParameters.Companion.DOKKA_VERSIONING_PLUGIN_PARAMETERS_NAME +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.uppercaseFirstChar +import org.jetbrains.dokka.dokkatoo.tasks.LogHtmlPublicationLinkTask +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.* + +abstract class DokkatooHtmlPlugin +@DokkatooInternalApi +constructor() : DokkatooFormatPlugin(formatName = "html") { + + override fun DokkatooFormatPluginContext.configure() { + registerDokkaBasePluginConfiguration() + registerDokkaVersioningPlugin() + + val logHtmlUrlTask = registerLogHtmlUrlTask() + + dokkatooTasks.generatePublication.configure { + finalizedBy(logHtmlUrlTask) + } + } + + private fun DokkatooFormatPluginContext.registerDokkaBasePluginConfiguration() { + with(dokkatooExtension.pluginsConfiguration) { + registerBinding(DokkaHtmlPluginParameters::class, DokkaHtmlPluginParameters::class) + register<DokkaHtmlPluginParameters>(DOKKA_HTML_PARAMETERS_NAME) + withType<DokkaHtmlPluginParameters>().configureEach { + separateInheritedMembers.convention(false) + mergeImplicitExpectActualDeclarations.convention(false) + } + } + } + + private fun DokkatooFormatPluginContext.registerDokkaVersioningPlugin() { + // register and configure Dokka Versioning Plugin + with(dokkatooExtension.pluginsConfiguration) { + registerBinding( + DokkaVersioningPluginParameters::class, + DokkaVersioningPluginParameters::class, + ) + register<DokkaVersioningPluginParameters>(DOKKA_VERSIONING_PLUGIN_PARAMETERS_NAME) + withType<DokkaVersioningPluginParameters>().configureEach { + renderVersionsNavigationOnAllPages.convention(true) + } + } + } + + private fun DokkatooFormatPluginContext.registerLogHtmlUrlTask(): + TaskProvider<LogHtmlPublicationLinkTask> { + + val indexHtmlFile = dokkatooTasks.generatePublication + .flatMap { it.outputDirectory.file("index.html") } + + val indexHtmlPath = indexHtmlFile.map { indexHtml -> + indexHtml.asFile + .relativeTo(project.rootDir.parentFile) + .invariantSeparatorsPath + } + + return project.tasks.register<LogHtmlPublicationLinkTask>( + "logLink" + dokkatooTasks.generatePublication.name.uppercaseFirstChar() + ) { + serverUri.convention("http://localhost:63342") + this.indexHtmlPath.convention(indexHtmlPath) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJavadocPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJavadocPlugin.kt new file mode 100644 index 00000000..90f024df --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJavadocPlugin.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.gradle.kotlin.dsl.* + +abstract class DokkatooJavadocPlugin +@DokkatooInternalApi +constructor() : DokkatooFormatPlugin(formatName = "javadoc") { + override fun DokkatooFormatPluginContext.configure() { + project.dependencies { + dokkaPlugin(dokka("javadoc-plugin")) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJekyllPlugin.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJekyllPlugin.kt new file mode 100644 index 00000000..d8434732 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/formats/DokkatooJekyllPlugin.kt @@ -0,0 +1,14 @@ +package org.jetbrains.dokka.dokkatoo.formats + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.gradle.kotlin.dsl.* + +abstract class DokkatooJekyllPlugin +@DokkatooInternalApi +constructor() : DokkatooFormatPlugin(formatName = "jekyll") { + override fun DokkatooFormatPluginContext.configure() { + project.dependencies { + dokkaPlugin(dokka("jekyll-plugin")) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/DokkatooInternalApi.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/DokkatooInternalApi.kt new file mode 100644 index 00000000..e3e63753 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/DokkatooInternalApi.kt @@ -0,0 +1,37 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import kotlin.RequiresOptIn.Level.WARNING +import kotlin.annotation.AnnotationRetention.BINARY +import kotlin.annotation.AnnotationTarget.* + + +/** + * Functionality that is annotated with this API is intended only for use by Dokkatoo internal code, + * but it has been given + * [`public` visibility](https://kotlinlang.org/docs/visibility-modifiers.html) + * for technical reasons. + * + * Any code that is annotated with this may be used + * + * Anyone is welcome to + * [opt in](https://kotlinlang.org/docs/opt-in-requirements.html#opt-in-to-using-api) + * to use this API, but be aware that it might change unexpectedly and without warning or migration + * hints. + * + * If you find yourself needing to opt in, then please report your use-case on + * [the Dokkatoo issue tracker](https://github.com/adamko-dev/dokkatoo/issues). + */ +@RequiresOptIn( + "Internal API - may change at any time without notice", + level = WARNING +) +@Retention(BINARY) +@Target( + CLASS, + FUNCTION, + CONSTRUCTOR, + PROPERTY, + PROPERTY_GETTER, +) +@MustBeDocumented +annotation class DokkatooInternalApi diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/LoggerAdapter.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/LoggerAdapter.kt new file mode 100644 index 00000000..0a1b94fc --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/LoggerAdapter.kt @@ -0,0 +1,65 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import java.io.File +import java.io.Writer +import java.util.concurrent.atomic.AtomicInteger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel + +/** + * Logs all Dokka messages to a file. + * + * @see org.jetbrains.dokka.DokkaGenerator + */ +// Gradle causes OOM errors when there is a lot of console output. Logging to file is a workaround. +// https://github.com/gradle/gradle/issues/23965 +// https://github.com/gradle/gradle/issues/15621 +internal class LoggerAdapter( + outputFile: File +) : DokkaLogger, AutoCloseable { + + private val logWriter: Writer + + init { + if (!outputFile.exists()) { + outputFile.parentFile.mkdirs() + outputFile.createNewFile() + } + + logWriter = outputFile.bufferedWriter() + } + + private val warningsCounter = AtomicInteger() + private val errorsCounter = AtomicInteger() + + override var warningsCount: Int + get() = warningsCounter.get() + set(value) = warningsCounter.set(value) + + override var errorsCount: Int + get() = errorsCounter.get() + set(value) = errorsCounter.set(value) + + override fun debug(message: String) = log(LoggingLevel.DEBUG, message) + override fun progress(message: String) = log(LoggingLevel.PROGRESS, message) + override fun info(message: String) = log(LoggingLevel.INFO, message) + + override fun warn(message: String) { + warningsCount++ + log(LoggingLevel.WARN, message) + } + + override fun error(message: String) { + errorsCount++ + log(LoggingLevel.ERROR, message) + } + + @Synchronized + private fun log(level: LoggingLevel, message: String) { + logWriter.appendLine("[${level.name}] $message") + } + + override fun close() { + logWriter.close() + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/collectionsUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/collectionsUtils.kt new file mode 100644 index 00000000..80b66f4b --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/collectionsUtils.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.dokkatoo.internal + +internal fun <T, R> Set<T>.mapToSet(transform: (T) -> R): Set<R> = + mapTo(mutableSetOf(), transform) + +internal fun <T, R : Any> Set<T>.mapNotNullToSet(transform: (T) -> R?): Set<R> = + mapNotNullTo(mutableSetOf(), transform) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleExtensionAccessors.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleExtensionAccessors.kt new file mode 100644 index 00000000..85208897 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleExtensionAccessors.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import org.jetbrains.dokka.dokkatoo.DokkatooExtension + +// When Dokkatoo is applied to a build script Gradle will auto-generate these accessors + +internal fun DokkatooExtension.versions(configure: DokkatooExtension.Versions.() -> Unit) { + versions.apply(configure) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleTypealiases.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleTypealiases.kt new file mode 100644 index 00000000..7f59db86 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleTypealiases.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaPluginParametersBaseSpec +import org.gradle.api.ExtensiblePolymorphicDomainObjectContainer + +/** Container for all [Dokka Plugin parameters][DokkaPluginParametersBaseSpec]. */ +typealias DokkaPluginParametersContainer = + ExtensiblePolymorphicDomainObjectContainer<DokkaPluginParametersBaseSpec> + + +/** + * The path of a Gradle [Project][org.gradle.api.Project]. This is unique per subproject. + * This is _not_ the file path, which + * [can be configured to be different to the project path](https://docs.gradle.org/current/userguide/fine_tuning_project_layout.html#sub:modifying_element_of_the_project_tree). + * + * Example: `:modules:tests:alpha-project`. + * + * @see org.gradle.api.Project.getPath + */ +internal typealias GradleProjectPath = org.gradle.util.Path diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt new file mode 100644 index 00000000..53ba49b9 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/gradleUtils.kt @@ -0,0 +1,187 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import org.jetbrains.dokka.dokkatoo.dokka.plugins.DokkaPluginParametersBaseSpec +import org.gradle.api.* +import org.gradle.api.artifacts.ArtifactView +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ConfigurationContainer +import org.gradle.api.artifacts.component.ComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.api.provider.Provider +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.* + + +/** + * Mark this [Configuration] as one that will be consumed by other subprojects. + * + * ``` + * isCanBeResolved = false + * isCanBeConsumed = true + * ``` + */ +internal fun Configuration.asProvider( + visible: Boolean = true, +) { + isCanBeResolved = false + isCanBeConsumed = true + isVisible = visible +} + +/** + * Mark this [Configuration] as one that will consume artifacts from other subprojects (also known as 'resolving') + * + * ``` + * isCanBeResolved = true + * isCanBeConsumed = false + * ``` + * */ +internal fun Configuration.asConsumer( + visible: Boolean = false, +) { + isCanBeResolved = true + isCanBeConsumed = false + isVisible = visible +} + + +/** Invert a boolean [Provider] */ +internal operator fun Provider<Boolean>.not(): Provider<Boolean> = map { !it } + + +/** Only matches components that come from subprojects */ +internal object LocalProjectOnlyFilter : Spec<ComponentIdentifier> { + override fun isSatisfiedBy(element: ComponentIdentifier?): Boolean = + element is ProjectComponentIdentifier +} + + +/** Invert the result of a [Spec] predicate */ +internal operator fun <T> Spec<T>.not(): Spec<T> = Spec<T> { !this@not.isSatisfiedBy(it) } + + +internal fun Project.pathAsFilePath() = path + .removePrefix(GradleProjectPath.SEPARATOR) + .replace(GradleProjectPath.SEPARATOR, "/") + + +/** + * Apply some configuration to a [Task] using + * [configure][org.gradle.api.tasks.TaskContainer.configure], + * and return the same [TaskProvider]. + */ +internal fun <T : Task> TaskProvider<T>.configuring( + block: Action<T> +): TaskProvider<T> = apply { configure(block) } + + +internal fun <T> NamedDomainObjectContainer<T>.maybeCreate( + name: String, + configure: T.() -> Unit, +): T = maybeCreate(name).apply(configure) + + +/** + * Aggregate the incoming files from a [Configuration] (with name [named]) into [collector]. + * + * Configurations that do not exist or cannot be + * [resolved][org.gradle.api.artifacts.Configuration.isCanBeResolved] + * will be ignored. + * + * @param[builtBy] An optional [TaskProvider], used to set [ConfigurableFileCollection.builtBy]. + * This should not typically be used, and is only necessary in rare cases where a Gradle Plugin is + * misconfigured. + */ +internal fun ConfigurationContainer.collectIncomingFiles( + named: String, + collector: ConfigurableFileCollection, + builtBy: TaskProvider<*>? = null, + artifactViewConfiguration: ArtifactView.ViewConfiguration.() -> Unit = { + // ignore failures: it's usually okay if fetching files is best-effort because + // maybe Dokka doesn't need _all_ dependencies + lenient(true) + }, +) { + val conf = findByName(named) + if (conf != null && conf.isCanBeResolved) { + val incomingFiles = conf.incoming + .artifactView(artifactViewConfiguration) + .artifacts + .resolvedArtifacts // using 'resolved' might help with triggering artifact transforms? + .map { artifacts -> artifacts.map { it.file } } + + collector.from(incomingFiles) + + if (builtBy != null) { + collector.builtBy(builtBy) + } + } +} + + +/** + * Create a new [NamedDomainObjectContainer], using + * [org.gradle.kotlin.dsl.domainObjectContainer] + * (but [T] is `reified`). + * + * @param[factory] an optional factory for creating elements + * @see org.gradle.kotlin.dsl.domainObjectContainer + */ +internal inline fun <reified T : Any> ObjectFactory.domainObjectContainer( + factory: NamedDomainObjectFactory<T>? = null +): NamedDomainObjectContainer<T> = + if (factory == null) { + domainObjectContainer(T::class) + } else { + domainObjectContainer(T::class, factory) + } + + +/** + * Create a new [ExtensiblePolymorphicDomainObjectContainer], using + * [org.gradle.kotlin.dsl.polymorphicDomainObjectContainer] + * (but [T] is `reified`). + * + * @see org.gradle.kotlin.dsl.polymorphicDomainObjectContainer + */ +internal inline fun <reified T : Any> ObjectFactory.polymorphicDomainObjectContainer() + : ExtensiblePolymorphicDomainObjectContainer<T> = + polymorphicDomainObjectContainer(T::class) + + +/** + * Add an extension to the [ExtensionContainer], and return the value. + * + * Adding an extension is especially useful for improving the DSL in build scripts when [T] is a + * [NamedDomainObjectContainer]. + * Using an extension will allow Gradle to generate + * [type-safe model accessors](https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:accessor_applicability) + * for added types. + * + * ([name] should match the property name. This has to be done manually. I tried using a + * delegated-property provider but then Gradle can't introspect the types properly, so it fails to + * create accessors). + */ +internal inline fun <reified T : Any> ExtensionContainer.adding( + name: String, + value: T, +): T { + add<T>(name, value) + return value +} + + +/** Create a new [DokkaPluginParametersContainer] instance. */ +internal fun ObjectFactory.dokkaPluginParametersContainer(): DokkaPluginParametersContainer { + val container = polymorphicDomainObjectContainer<DokkaPluginParametersBaseSpec>() + container.whenObjectAdded { + // workaround for https://github.com/gradle/gradle/issues/24972 + (container as ExtensionAware).extensions.add(name, this) + } + return container +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/kotlinxSerializationUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/kotlinxSerializationUtils.kt new file mode 100644 index 00000000..d4f98004 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/kotlinxSerializationUtils.kt @@ -0,0 +1,36 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import java.io.File +import kotlinx.serialization.json.JsonArrayBuilder +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add + + +@JvmName("addAllFiles") +internal fun JsonArrayBuilder.addAll(files: Iterable<File>) { + files + .map { it.canonicalFile.invariantSeparatorsPath } + .forEach { path -> add(path) } +} + +@JvmName("addAllStrings") +internal fun JsonArrayBuilder.addAll(values: Iterable<String>) { + values.forEach { add(it) } +} + +internal fun JsonArrayBuilder.addAllIfNotNull(values: Iterable<String>?) { + if (values != null) addAll(values) +} + +internal fun JsonObjectBuilder.putIfNotNull(key: String, value: Boolean?) { + if (value != null) put(key, JsonPrimitive(value)) +} + +internal fun JsonObjectBuilder.putIfNotNull(key: String, value: String?) { + if (value != null) put(key, JsonPrimitive(value)) +} + +internal fun JsonObjectBuilder.putIfNotNull(key: String, value: File?) { + if (value != null) put(key, JsonPrimitive(value.canonicalFile.invariantSeparatorsPath)) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/stringUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/stringUtils.kt new file mode 100644 index 00000000..75b3b8ec --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/stringUtils.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.dokkatoo.internal + + +/** + * Title case the first char of a string. + * + * (Custom implementation because [uppercase] is deprecated, and Dokkatoo should try and be as + * stable as possible.) + */ +internal fun String.uppercaseFirstChar(): String = + if (isNotEmpty()) Character.toTitleCase(this[0]) + substring(1) else this diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/uriUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/uriUtils.kt new file mode 100644 index 00000000..942551c4 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/internal/uriUtils.kt @@ -0,0 +1,9 @@ +package org.jetbrains.dokka.dokkatoo.internal + +import java.net.URI + +internal fun URI.appendPath(addition: String): URI { + val currentPath = path.removeSuffix("/") + val newPath = "$currentPath/$addition" + return resolve(newPath).normalize() +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooGenerateTask.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooGenerateTask.kt new file mode 100644 index 00000000..b27acbc5 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooGenerateTask.kt @@ -0,0 +1,187 @@ +package org.jetbrains.dokka.dokkatoo.tasks + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin.Companion.jsonMapper +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaGeneratorParametersSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaModuleDescriptionKxs +import org.jetbrains.dokka.dokkatoo.dokka.parameters.builders.DokkaParametersBuilder +import org.jetbrains.dokka.dokkatoo.internal.DokkaPluginParametersContainer +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.workers.DokkaGeneratorWorker +import java.io.IOException +import javax.inject.Inject +import kotlinx.serialization.json.JsonElement +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.model.ReplacedBy +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.* +import org.gradle.process.JavaForkOptions +import org.gradle.workers.WorkerExecutor +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.toPrettyJsonString + +/** + * Executes the Dokka Generator, and produces documentation. + * + * The type of documentation generated is determined by the supplied Dokka Plugins in [generator]. + */ +@CacheableTask +abstract class DokkatooGenerateTask +@DokkatooInternalApi +@Inject +constructor( + objects: ObjectFactory, + private val workers: WorkerExecutor, + + /** + * Configurations for Dokka Generator Plugins. Must be provided from + * [org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication.pluginsConfiguration]. + */ + pluginsConfiguration: DokkaPluginParametersContainer, +) : DokkatooTask() { + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + /** + * Classpath required to run Dokka Generator. + * + * Contains the Dokka Generator, Dokka plugins, and any transitive dependencies. + */ + @get:Classpath + abstract val runtimeClasspath: ConfigurableFileCollection + + @get:LocalState + abstract val cacheDirectory: DirectoryProperty + + /** + * Generating a Dokka Module? Set this to [GenerationType.MODULE]. + * + * Generating a Dokka Publication? [GenerationType.PUBLICATION]. + */ + @get:Input + abstract val generationType: Property<GenerationType> + + /** @see org.jetbrains.dokka.dokkatoo.dokka.DokkaPublication.enabled */ + @get:Input + abstract val publicationEnabled: Property<Boolean> + + @get:Nested + val generator: DokkaGeneratorParametersSpec = objects.newInstance(pluginsConfiguration) + + /** @see JavaForkOptions.getDebug */ + @get:Input + abstract val workerDebugEnabled: Property<Boolean> + /** @see JavaForkOptions.getMinHeapSize */ + @get:Input + @get:Optional + abstract val workerMinHeapSize: Property<String> + /** @see JavaForkOptions.getMaxHeapSize */ + @get:Input + @get:Optional + abstract val workerMaxHeapSize: Property<String> + /** @see JavaForkOptions.jvmArgs */ + @get:Input + abstract val workerJvmArgs: ListProperty<String> + @get:Internal + abstract val workerLogFile: RegularFileProperty + + /** + * The [DokkaConfiguration] by Dokka Generator can be saved to a file for debugging purposes. + * To disable this behaviour set this property to `null`. + */ + @DokkatooInternalApi + @get:Internal + abstract val dokkaConfigurationJsonFile: RegularFileProperty + + enum class GenerationType { + MODULE, + PUBLICATION, + } + + @TaskAction + internal fun generateDocumentation() { + val dokkaConfiguration = createDokkaConfiguration() + logger.info("dokkaConfiguration: $dokkaConfiguration") + dumpDokkaConfigurationJson(dokkaConfiguration) + + logger.info("DokkaGeneratorWorker runtimeClasspath: ${runtimeClasspath.asPath}") + + val workQueue = workers.processIsolation { + classpath.from(runtimeClasspath) + forkOptions { + defaultCharacterEncoding = "UTF-8" + minHeapSize = workerMinHeapSize.orNull + maxHeapSize = workerMaxHeapSize.orNull + enableAssertions = true + debug = workerDebugEnabled.get() + jvmArgs = workerJvmArgs.get() + } + } + + workQueue.submit(DokkaGeneratorWorker::class) { + this.dokkaParameters.set(dokkaConfiguration) + this.logFile.set(workerLogFile) + } + } + + /** + * Dump the [DokkaConfiguration] JSON to a file ([dokkaConfigurationJsonFile]) for debugging + * purposes. + */ + private fun dumpDokkaConfigurationJson( + dokkaConfiguration: DokkaConfiguration, + ) { + val destFile = dokkaConfigurationJsonFile.asFile.orNull ?: return + destFile.parentFile.mkdirs() + destFile.createNewFile() + + val compactJson = dokkaConfiguration.toPrettyJsonString() + val json = jsonMapper.decodeFromString(JsonElement.serializer(), compactJson) + val prettyJson = jsonMapper.encodeToString(JsonElement.serializer(), json) + + destFile.writeText(prettyJson) + + logger.info("[$path] Dokka Generator configuration JSON: ${destFile.toURI()}") + } + + private fun createDokkaConfiguration(): DokkaConfiguration { + val outputDirectory = outputDirectory.get().asFile + + val delayTemplateSubstitution = when (generationType.orNull) { + GenerationType.MODULE -> true + GenerationType.PUBLICATION -> false + null -> error("missing GenerationType") + } + + val dokkaModuleDescriptors = dokkaModuleDescriptors() + + return DokkaParametersBuilder.build( + spec = generator, + delayTemplateSubstitution = delayTemplateSubstitution, + outputDirectory = outputDirectory, + modules = dokkaModuleDescriptors, + cacheDirectory = cacheDirectory.asFile.orNull, + ) + } + + private fun dokkaModuleDescriptors(): List<DokkaModuleDescriptionKxs> { + return generator.dokkaModuleFiles.asFileTree + .matching { include("**/module_descriptor.json") } + .files.map { file -> + try { + val fileContent = file.readText() + jsonMapper.decodeFromString( + DokkaModuleDescriptionKxs.serializer(), + fileContent, + ) + } catch (ex: Exception) { + throw IOException("Could not parse DokkaModuleDescriptionKxs from $file", ex) + } + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareModuleDescriptorTask.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareModuleDescriptorTask.kt new file mode 100644 index 00000000..1247ebc7 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooPrepareModuleDescriptorTask.kt @@ -0,0 +1,62 @@ +package org.jetbrains.dokka.dokkatoo.tasks + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin.Companion.jsonMapper +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaModuleDescriptionKxs +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import javax.inject.Inject +import kotlinx.serialization.encodeToString +import org.gradle.api.file.* +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.api.tasks.PathSensitivity.RELATIVE + +/** + * Produces a Dokka Configuration that describes a single module of a multimodule Dokka configuration. + * + * @see org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaModuleDescriptionKxs + */ +@CacheableTask +abstract class DokkatooPrepareModuleDescriptorTask +@DokkatooInternalApi +@Inject +constructor() : DokkatooTask() { + + @get:OutputFile + abstract val dokkaModuleDescriptorJson: RegularFileProperty + + @get:Input + abstract val moduleName: Property<String> + + @get:Input + abstract val modulePath: Property<String> + + @get:InputDirectory + @get:PathSensitive(RELATIVE) + abstract val moduleDirectory: DirectoryProperty + + @get:InputFiles + @get:Optional + @get:PathSensitive(RELATIVE) + abstract val includes: ConfigurableFileCollection + + @TaskAction + internal fun generateModuleConfiguration() { + val moduleName = moduleName.get() + val moduleDirectory = moduleDirectory.asFile.get() + val includes = includes.files + val modulePath = modulePath.get() + + val moduleDesc = DokkaModuleDescriptionKxs( + name = moduleName, + sourceOutputDirectory = moduleDirectory, + includes = includes, + modulePath = modulePath, + ) + + val encodedModuleDesc = jsonMapper.encodeToString(moduleDesc) + + logger.info("encodedModuleDesc: $encodedModuleDesc") + + dokkaModuleDescriptorJson.get().asFile.writeText(encodedModuleDesc) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooTask.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooTask.kt new file mode 100644 index 00000000..c125a64e --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/DokkatooTask.kt @@ -0,0 +1,22 @@ +package org.jetbrains.dokka.dokkatoo.tasks + +import org.jetbrains.dokka.dokkatoo.DokkatooBasePlugin +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.CacheableTask + +/** Base Dokkatoo task */ +@CacheableTask +abstract class DokkatooTask +@DokkatooInternalApi +constructor() : DefaultTask() { + + @get:Inject + abstract val objects: ObjectFactory + + init { + group = DokkatooBasePlugin.TASK_GROUP + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/LogHtmlPublicationLinkTask.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/LogHtmlPublicationLinkTask.kt new file mode 100644 index 00000000..c281ce56 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/tasks/LogHtmlPublicationLinkTask.kt @@ -0,0 +1,156 @@ +package org.jetbrains.dokka.dokkatoo.tasks + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.appendPath +import org.jetbrains.dokka.dokkatoo.tasks.LogHtmlPublicationLinkTask.Companion.ENABLE_TASK_PROPERTY_NAME +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration +import javax.inject.Inject +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.api.tasks.Console +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.* +import org.gradle.work.DisableCachingByDefault + +/** + * Prints an HTTP link in the console when the HTML publication is generated. + * + * The HTML publication requires a web server, since it loads resources via javascript. + * + * By default, it uses + * [IntelliJ's built-in server](https://www.jetbrains.com/help/idea/php-built-in-web-server.html) + * to host the file. + * + * This task can be disabled using the [ENABLE_TASK_PROPERTY_NAME] project property. + */ +@DisableCachingByDefault(because = "logging-only task") +abstract class LogHtmlPublicationLinkTask +@Inject +@DokkatooInternalApi +constructor( + providers: ProviderFactory +) : DokkatooTask() { + + @get:Console + abstract val serverUri: Property<String> + + /** + * Path to the `index.html` of the publication. Will be appended to [serverUri]. + * + * The IntelliJ built-in server requires a relative path originating from the _parent_ directory + * of the IntelliJ project. + * + * For example, + * + * * given an IntelliJ project path of + * ``` + * /Users/rachel/projects/my-project/ + * ``` + * * and the publication is generated with an index file + * ``` + * /Users/rachel/projects/my-project/docs/build/dokka/html/index.html + * ```` + * * then IntelliJ requires the [indexHtmlPath] is + * ``` + * my-project/docs/build/dokka/html/index.html + * ``` + * * so that (assuming [serverUri] is `http://localhost:63342`) the logged URL is + * ``` + * http://localhost:63342/my-project/docs/build/dokka/html/index.html + * ``` + */ + @get:Console + abstract val indexHtmlPath: Property<String> + + init { + // don't assign a group. This task is a 'finalizer' util task, so it doesn't make sense + // to display this task prominently. + group = "other" + + val serverActive = providers.of(ServerActiveCheck::class) { + parameters.uri.convention(serverUri) + } + super.onlyIf("server URL is reachable") { serverActive.get() } + + val logHtmlPublicationLinkTaskEnabled = providers + .gradleProperty(ENABLE_TASK_PROPERTY_NAME) + .orElse("true") + .map(String::toBoolean) + super.onlyIf("task is enabled via property") { + logHtmlPublicationLinkTaskEnabled.get() + } + } + + @TaskAction + fun exec() { + val serverUri = serverUri.orNull + val filePath = indexHtmlPath.orNull + + if (serverUri != null && !filePath.isNullOrBlank()) { + val link = URI(serverUri).appendPath(filePath).toString() + + logger.lifecycle("Generated Dokka HTML publication: $link") + } + } + + /** + * Check if the server URI that can host the generated Dokka HTML publication is accessible. + * + * Use the [HttpClient] included with Java 11 to avoid bringing in a new dependency for such + * a small util. + * + * The check uses a [ValueSource] source to attempt to be compatible with Configuration Cache, but + * I'm not certain that this is necessary, or if a [ValueSource] is the best way to achieve it. + */ + internal abstract class ServerActiveCheck : ValueSource<Boolean, ServerActiveCheck.Parameters> { + + interface Parameters : ValueSourceParameters { + /** E.g. `http://localhost:63342` */ + val uri: Property<String> + } + + override fun obtain(): Boolean { + try { + val uri = URI.create(parameters.uri.get()) + val client = HttpClient.newHttpClient() + val request = HttpRequest + .newBuilder() + .uri(uri) + .timeout(Duration.ofSeconds(1)) + .GET() + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + // don't care about the status - only if the server is available + return response.statusCode() > 0 + } catch (ex: Exception) { + return false + } + } + } + + companion object { + /** + * Control whether the [LogHtmlPublicationLinkTask] task is enabled. Useful for disabling the + * task locally, or in CI/CD, or for tests. + * + * ```properties + * #$GRADLE_USER_HOME/gradle.properties + * org.jetbrains.dokka.dokkatoo.tasks.logHtmlPublicationLinkEnabled=false + * ``` + * + * or via an environment variable + * + * ```env + * ORG_GRADLE_PROJECT_org.jetbrains.dokka.dokkatoo.tasks.logHtmlPublicationLinkEnabled=false + * ``` + */ + const val ENABLE_TASK_PROPERTY_NAME = "org.jetbrains.dokka.dokkatoo.tasks.logHtmlPublicationLinkEnabled" + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt new file mode 100644 index 00000000..4ac58d03 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/main/kotlin/workers/DokkaGeneratorWorker.kt @@ -0,0 +1,77 @@ +package org.jetbrains.dokka.dokkatoo.workers + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooInternalApi +import org.jetbrains.dokka.dokkatoo.internal.LoggerAdapter +import java.io.File +import java.time.Duration +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaGenerator + +/** + * Gradle Worker Daemon for running [DokkaGenerator]. + * + * The worker requires [DokkaGenerator] is present on its classpath, as well as any Dokka plugins + * that are used to generate the Dokka files. Transitive dependencies are also required. + */ +@DokkatooInternalApi +abstract class DokkaGeneratorWorker : WorkAction<DokkaGeneratorWorker.Parameters> { + + @DokkatooInternalApi + interface Parameters : WorkParameters { + val dokkaParameters: Property<DokkaConfiguration> + val logFile: RegularFileProperty + } + + override fun execute() { + val dokkaParameters = parameters.dokkaParameters.get() + + prepareOutputDir(dokkaParameters) + + executeDokkaGenerator( + parameters.logFile.get().asFile, + dokkaParameters, + ) + } + + private fun prepareOutputDir(dokkaParameters: DokkaConfiguration) { + // Dokka Generator doesn't clean up old files, so we need to manually clean the output directory + dokkaParameters.outputDir.deleteRecursively() + dokkaParameters.outputDir.mkdirs() + + // workaround until https://github.com/Kotlin/dokka/pull/2867 is released + dokkaParameters.modules.forEach { module -> + val moduleDir = dokkaParameters.outputDir.resolve(module.relativePathToOutputDirectory) + moduleDir.mkdirs() + } + } + + private fun executeDokkaGenerator( + logFile: File, + dokkaParameters: DokkaConfiguration + ) { + LoggerAdapter(logFile).use { logger -> + logger.progress("Executing DokkaGeneratorWorker with dokkaParameters: $dokkaParameters") + + val generator = DokkaGenerator(dokkaParameters, logger) + + val duration = measureTime { generator.generate() } + + logger.info("DokkaGeneratorWorker completed in $duration") + } + } + + @DokkatooInternalApi + companion object { + // can't use kotlin.Duration or kotlin.time.measureTime {} because + // the implementation isn't stable across Kotlin versions + private fun measureTime(block: () -> Unit): Duration = + System.nanoTime().let { startTime -> + block() + Duration.ofNanos(System.nanoTime() - startTime) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/DokkatooPluginTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/DokkatooPluginTest.kt new file mode 100644 index 00000000..843708a3 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/DokkatooPluginTest.kt @@ -0,0 +1,76 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.utils.create_ +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldEndWith +import org.gradle.kotlin.dsl.* +import org.gradle.testfixtures.ProjectBuilder + +class DokkatooPluginTest : FunSpec({ + + test("expect plugin id can be applied to project successfully") { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka.dokkatoo") + project.plugins.hasPlugin("org.jetbrains.dokka.dokkatoo") shouldBe true + project.plugins.hasPlugin(DokkatooPlugin::class) shouldBe true + } + + test("expect plugin class can be applied to project successfully") { + val project = ProjectBuilder.builder().build() + project.plugins.apply(type = DokkatooPlugin::class) + project.plugins.hasPlugin("org.jetbrains.dokka.dokkatoo") shouldBe true + project.plugins.hasPlugin(DokkatooPlugin::class) shouldBe true + } + + context("Dokkatoo property conventions") { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka.dokkatoo") + + val extension = project.extensions.getByType<DokkatooExtension>() + + context("DokkatooSourceSets") { + val testSourceSet = extension.dokkatooSourceSets.create_("Test") { + externalDocumentationLinks.create_("gradle") { + url("https://docs.gradle.org/7.6.1/javadoc") + } + } + + context("JDK external documentation link") { + val jdkLink = testSourceSet.externalDocumentationLinks.getByName("jdk") + + test("when enableJdkDocumentationLink is false, expect jdk link is disabled") { + testSourceSet.enableJdkDocumentationLink.set(false) + jdkLink.enabled.get() shouldBe false + } + + test("when enableJdkDocumentationLink is true, expect jdk link is enabled") { + testSourceSet.enableJdkDocumentationLink.set(true) + jdkLink.enabled.get() shouldBe true + } + + (5..10).forEach { jdkVersion -> + test("when jdkVersion is $jdkVersion, expect packageListUrl uses package-list file") { + testSourceSet.jdkVersion.set(jdkVersion) + jdkLink.packageListUrl.get().toString() shouldEndWith "package-list" + } + } + + (11..22).forEach { jdkVersion -> + test("when jdkVersion is $jdkVersion, expect packageListUrl uses element-list file") { + testSourceSet.jdkVersion.set(jdkVersion) + jdkLink.packageListUrl.get().toString() shouldEndWith "element-list" + } + } + } + + context("external doc links") { + test("package-list url should be appended to Javadoc URL") { + val gradleDocLink = testSourceSet.externalDocumentationLinks.getByName("gradle") + gradleDocLink.packageListUrl.get() + .toString() shouldBe "https://docs.gradle.org/7.6.1/javadoc/package-list" + } + } + } + } +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpecTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpecTest.kt new file mode 100644 index 00000000..28fb2b83 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaExternalDocumentationLinkSpecTest.kt @@ -0,0 +1,102 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.DokkatooPlugin +import org.jetbrains.dokka.dokkatoo.utils.create_ +import io.kotest.core.spec.style.FunSpec +import io.kotest.datatest.WithDataTestName +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe +import org.gradle.kotlin.dsl.* +import org.gradle.testfixtures.ProjectBuilder + + +class DokkaExternalDocumentationLinkSpecTest : FunSpec({ + + context("expect url can be set") { + test("using a string") { + val actual = createExternalDocLinkSpec { + url("https://github.com/adamko-dev/dokkatoo/") + } + + actual.url.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + + test("using a string-provider") { + val actual = createExternalDocLinkSpec { + url(project.provider { "https://github.com/adamko-dev/dokkatoo/" }) + } + + actual.url.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + } + + context("expect packageListUrl can be set") { + test("using a string") { + val actual = createExternalDocLinkSpec { + packageListUrl("https://github.com/adamko-dev/dokkatoo/") + } + + actual.packageListUrl.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + + test("using a string-provider") { + val actual = createExternalDocLinkSpec { + packageListUrl(project.provider { "https://github.com/adamko-dev/dokkatoo/" }) + } + + actual.packageListUrl.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + } + + context("expect packageList defaults to url+package-list") { + data class TestCase( + val actualUrl: String, + val expected: String, + val testName: String, + ) : WithDataTestName { + override fun dataTestName(): String = testName + } + + withData( + TestCase( + testName = "non-empty path, with trailing slash", + actualUrl = "https://github.com/adamko-dev/dokkatoo/", + expected = "https://github.com/adamko-dev/dokkatoo/package-list", + ), + TestCase( + testName = "non-empty path, without trailing slash", + actualUrl = "https://github.com/adamko-dev/dokkatoo", + expected = "https://github.com/adamko-dev/dokkatoo/package-list", + ), + TestCase( + testName = "empty path, with trailing slash", + actualUrl = "https://github.com/", + expected = "https://github.com/package-list", + ), + TestCase( + testName = "empty path, without trailing slash", + actualUrl = "https://github.com", + expected = "https://github.com/package-list", + ) + ) { (actualUrl, expected) -> + val actual = createExternalDocLinkSpec { url(actualUrl) } + actual.packageListUrl.get().toString() shouldBe expected + } + } +}) + +private val project = ProjectBuilder.builder().build().also { project -> + project.plugins.apply(type = DokkatooPlugin::class) +} + +private fun createExternalDocLinkSpec( + configure: DokkaExternalDocumentationLinkSpec.() -> Unit +): DokkaExternalDocumentationLinkSpec { + + val dssContainer = project.extensions.getByType<DokkatooExtension>().dokkatooSourceSets + + return dssContainer.create_("test" + dssContainer.size) + .externalDocumentationLinks + .create("testLink", configure) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaSourceLinkSpecTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaSourceLinkSpecTest.kt new file mode 100644 index 00000000..f3171a57 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/DokkaSourceLinkSpecTest.kt @@ -0,0 +1,58 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.* +import org.gradle.testfixtures.ProjectBuilder + +class DokkaSourceLinkSpecTest : FunSpec({ + val project = ProjectBuilder.builder().build() + + context("expect localDirectoryPath") { + test("is the invariantSeparatorsPath of localDirectory") { + val tempDir = tempdir() + + val actual = project.createDokkaSourceLinkSpec { + localDirectory.set(tempDir) + } + + actual.localDirectoryPath2.get() shouldBe tempDir.invariantSeparatorsPath + } + } + + + context("expect remoteUrl can be set") { + test("using a string") { + val actual = project.createDokkaSourceLinkSpec { + remoteUrl("https://github.com/adamko-dev/dokkatoo/") + } + + actual.remoteUrl.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + + test("using a string-provider") { + val actual = project.createDokkaSourceLinkSpec { + remoteUrl(project.provider { "https://github.com/adamko-dev/dokkatoo/" }) + } + + actual.remoteUrl.get().toString() shouldBe "https://github.com/adamko-dev/dokkatoo/" + } + } +}) { + + /** Re-implement [DokkaSourceLinkSpec] to make [localDirectoryPath] accessible in tests */ + abstract class DokkaSourceLinkSpec2 : DokkaSourceLinkSpec() { + val localDirectoryPath2: Provider<String> + get() = super.localDirectoryPath + } + + companion object { + private fun Project.createDokkaSourceLinkSpec( + configure: DokkaSourceLinkSpec.() -> Unit + ): DokkaSourceLinkSpec2 = + objects.newInstance(DokkaSourceLinkSpec2::class).apply(configure) + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt new file mode 100644 index 00000000..c921df9a --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/KotlinPlatformTest.kt @@ -0,0 +1,37 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.KotlinPlatform.Companion.dokkaType +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import org.jetbrains.dokka.Platform + +class KotlinPlatformTest : FunSpec({ + + test("should have same default as Dokka type") { + KotlinPlatform.DEFAULT.dokkaType shouldBe Platform.DEFAULT + } + + test("Dokka platform should have equivalent KotlinPlatform") { + + Platform.values().shouldForAll { dokkaPlatform -> + dokkaPlatform shouldBeIn KotlinPlatform.values.map { it.dokkaType } + } + } + + test("platform strings should map to same KotlinPlatform and Platform") { + listOf( + "androidJvm", + "android", + "metadata", + "jvm", + "js", + "wasm", + "native", + "common", + ).shouldForAll { + Platform.fromString(it) shouldBe KotlinPlatform.fromString(it).dokkaType + } + } +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/VisibilityModifierTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/VisibilityModifierTest.kt new file mode 100644 index 00000000..ca5ad49a --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/VisibilityModifierTest.kt @@ -0,0 +1,17 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.VisibilityModifier.Companion.dokkaType +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.inspectors.shouldForOne +import io.kotest.matchers.shouldBe +import org.jetbrains.dokka.DokkaConfiguration + +class VisibilityModifierTest : FunSpec({ + + test("DokkaConfiguration.Visibility should have equivalent VisibilityModifier") { + DokkaConfiguration.Visibility.values().shouldForAll { dokkaVisibility -> + VisibilityModifier.entries.map { it.dokkaType }.shouldForOne { it shouldBe dokkaVisibility } + } + } +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilderTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilderTest.kt new file mode 100644 index 00000000..ff442663 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaModuleDescriptionBuilderTest.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + +import io.kotest.core.spec.style.FunSpec + +class DokkaModuleDescriptionBuilderTest : FunSpec({ + +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaParametersBuilderTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaParametersBuilderTest.kt new file mode 100644 index 00000000..66918194 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaParametersBuilderTest.kt @@ -0,0 +1,7 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + +import io.kotest.core.spec.style.FunSpec + +class DokkaParametersBuilderTest : FunSpec({ + +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaSourceSetBuilderTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaSourceSetBuilderTest.kt new file mode 100644 index 00000000..bb4bf8a7 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/test/kotlin/dokka/parameters/builders/DokkaSourceSetBuilderTest.kt @@ -0,0 +1,198 @@ +package org.jetbrains.dokka.dokkatoo.dokka.parameters.builders + +import org.jetbrains.dokka.dokkatoo.DokkatooExtension +import org.jetbrains.dokka.dokkatoo.DokkatooPlugin +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.jetbrains.dokka.dokkatoo.utils.all_ +import org.jetbrains.dokka.dokkatoo.utils.create_ +import org.jetbrains.dokka.dokkatoo.utils.shouldContainAll +import org.jetbrains.dokka.dokkatoo.utils.sourceLink_ +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.spec.tempdir +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.equals.shouldNotBeEqual +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import java.io.File +import java.net.URI +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.internal.provider.MissingValueException +import org.gradle.kotlin.dsl.* +import org.gradle.testfixtures.ProjectBuilder + +class DokkaSourceSetBuilderTest : FunSpec({ + + context("when building a ExternalDocumentationLinkSpec") { + val project = createProject() + + test("expect url is required") { + val sourceSetSpec = project.createDokkaSourceSetSpec("test1") { + externalDocumentationLinks.create_("TestLink") { + url.set(null as URI?) + packageListUrl("https://github.com/adamko-dev/dokkatoo/") + } + } + + val caughtException = shouldThrow<MissingValueException> { + DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)) + } + + caughtException.message shouldContain "Cannot query the value of property 'url' because it has no value available" + } + + test("expect packageListUrl is required") { + val sourceSetSpec = project.createDokkaSourceSetSpec("test2") { + externalDocumentationLinks.create_("TestLink") { + url("https://github.com/adamko-dev/dokkatoo/") + packageListUrl.convention(null as URI?) + packageListUrl.set(null as URI?) + } + } + + val caughtException = shouldThrow<MissingValueException> { + DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)) + } + + caughtException.message shouldContain "Cannot query the value of property 'packageListUrl' because it has no value available" + } + + test("expect null when not enabled") { + val sourceSetSpec = project.createDokkaSourceSetSpec("test3") + val linkSpec = sourceSetSpec.externalDocumentationLinks.create_("TestLink") { + url("https://github.com/adamko-dev/dokkatoo/") + packageListUrl("https://github.com/adamko-dev/dokkatoo/") + enabled.set(false) + } + + DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)).shouldBeSingleton { sourceSet -> + sourceSet.externalDocumentationLinks.shouldForAll { link -> + link.url shouldNotBeEqual linkSpec.url.get().toURL() + link.packageListUrl shouldNotBeEqual linkSpec.packageListUrl.get().toURL() + } + } + } + } + + + context("when DokkaSourceLinkSpec is built") { + val project = createProject() + + test("expect built object contains all properties") { + val tempDir = tempdir() + + val sourceSetSpec = project.createDokkaSourceSetSpec("testAllProperties") { + sourceLink_ { + localDirectory.set(tempDir) + remoteUrl("https://github.com/adamko-dev/dokkatoo/") + remoteLineSuffix.set("%L") + } + } + + val sourceSet = DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)).single() + + sourceSet.sourceLinks.shouldBeSingleton { sourceLink -> + sourceLink.remoteUrl shouldBe URI("https://github.com/adamko-dev/dokkatoo/").toURL() + sourceLink.localDirectory shouldBe tempDir.invariantSeparatorsPath + sourceLink.remoteLineSuffix shouldBe "%L" + } + } + + test("expect localDirectory is required") { + val sourceSetSpec = project.createDokkaSourceSetSpec("testLocalDirRequired") { + sourceLink_ { + remoteUrl("https://github.com/adamko-dev/dokkatoo/") + remoteLineSuffix.set("%L") + } + } + + sourceSetSpec.sourceLinks.all_ { + localDirectory.convention(null as Directory?) + localDirectory.set(null as File?) + } + + val caughtException = shouldThrow<MissingValueException> { + DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)) + } + + caughtException.message.shouldContainAll( + "Cannot query the value of this provider because it has no value available", + "The value of this provider is derived from", + "property 'localDirectory'", + ) + } + + test("expect localDirectory is an invariantSeparatorsPath") { + val tempDir = tempdir() + + val sourceSetSpec = project.createDokkaSourceSetSpec("testLocalDirPath") { + sourceLink_ { + localDirectory.set(tempDir) + remoteUrl("https://github.com/adamko-dev/dokkatoo/") + remoteLineSuffix.set(null as String?) + } + } + + val link = DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)) + .single() + .sourceLinks + .single() + + link.localDirectory shouldBe tempDir.invariantSeparatorsPath + } + + test("expect remoteUrl is required") { + val sourceSetSpec = project.createDokkaSourceSetSpec("testRemoteUrlRequired") { + sourceLink_ { + localDirectory.set(tempdir()) + remoteUrl.set(project.providers.provider { null }) + remoteLineSuffix.set("%L") + } + } + + val caughtException = shouldThrow<MissingValueException> { + DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)) + } + + caughtException.message shouldContain "Cannot query the value of property 'remoteUrl' because it has no value available" + } + + test("expect remoteLineSuffix is optional") { + val tempDir = tempdir() + + val sourceSetSpec = project.createDokkaSourceSetSpec("testRemoteLineSuffixOptional") { + sourceLink_ { + localDirectory.set(tempDir) + remoteUrl("https://github.com/adamko-dev/dokkatoo/") + remoteLineSuffix.set(project.providers.provider { null }) + } + } + + val sourceSet = DokkaSourceSetBuilder.buildAll(setOf(sourceSetSpec)).single() + + sourceSet.sourceLinks.shouldBeSingleton { sourceLink -> + sourceLink.remoteUrl shouldBe URI("https://github.com/adamko-dev/dokkatoo/").toURL() + sourceLink.localDirectory shouldBe tempDir.invariantSeparatorsPath + sourceLink.remoteLineSuffix shouldBe null + } + } + } +}) + +private fun createProject(): Project { + val project = ProjectBuilder.builder().build() + project.plugins.apply(type = DokkatooPlugin::class) + return project +} + +private fun Project.createDokkaSourceSetSpec( + name: String, + configure: DokkaSourceSetSpec.() -> Unit = {} +): DokkaSourceSetSpec { + return extensions + .getByType<DokkatooExtension>() + .dokkatooSourceSets + .create_(name, configure) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt new file mode 100644 index 00000000..2f9e1b41 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/GradleTestKitUtils.kt @@ -0,0 +1,274 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import org.gradle.testkit.runner.GradleRunner +import org.intellij.lang.annotations.Language + + +// utils for testing using Gradle TestKit + + +class GradleProjectTest( + override val projectDir: Path, +) : ProjectDirectoryScope { + + constructor( + testProjectName: String, + baseDir: Path = funcTestTempDir, + ) : this(projectDir = baseDir.resolve(testProjectName)) + + val runner: GradleRunner + get() = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withJvmArguments( + "-XX:MaxMetaspaceSize=512m", + "-XX:+AlwaysPreTouch", // https://github.com/gradle/gradle/issues/3093#issuecomment-387259298 + ).addArguments( + // disable the logging task so the tests work consistently on local machines and CI/CD + "-P" + "org.jetbrains.dokka.dokkatoo.tasks.logHtmlPublicationLinkEnabled=false" + ) + + val testMavenRepoRelativePath: String = + projectDir.relativize(testMavenRepoDir).toFile().invariantSeparatorsPath + + companion object { + + /** file-based Maven Repo that contains the Dokka dependencies */ + val testMavenRepoDir: Path by systemProperty(Paths::get) + + val projectTestTempDir: Path by systemProperty(Paths::get) + + /** Temporary directory for the functional tests */ + val funcTestTempDir: Path by lazy { + projectTestTempDir.resolve("functional-tests") + } + + /** Dokka Source directory that contains Gradle projects used for integration tests */ + val integrationTestProjectsDir: Path by systemProperty(Paths::get) + /** Dokka Source directory that contains example Gradle projects */ + val exampleProjectsDir: Path by systemProperty(Paths::get) + } +} + + +///** +// * Load a project from the [GradleProjectTest.dokkaSrcIntegrationTestProjectsDir] +// */ +//fun gradleKtsProjectIntegrationTest( +// testProjectName: String, +// build: GradleProjectTest.() -> Unit, +//): GradleProjectTest = +// GradleProjectTest( +// baseDir = GradleProjectTest.dokkaSrcIntegrationTestProjectsDir, +// testProjectName = testProjectName, +// ).apply(build) + + +/** + * Builder for testing a Gradle project that uses Kotlin script DSL and creates default + * `settings.gradle.kts` and `gradle.properties` files. + * + * @param[testProjectName] the path of the project directory, relative to [baseDir + */ +fun gradleKtsProjectTest( + testProjectName: String, + baseDir: Path = GradleProjectTest.funcTestTempDir, + build: GradleProjectTest.() -> Unit, +): GradleProjectTest { + return GradleProjectTest(baseDir = baseDir, testProjectName = testProjectName).apply { + + settingsGradleKts = """ + |rootProject.name = "test" + | + |@Suppress("UnstableApiUsage") + |dependencyResolutionManagement { + | repositories { + | mavenCentral() + | maven(file("$testMavenRepoRelativePath")) { + | mavenContent { + | includeGroup("org.jetbrains.dokka.dokkatoo") + | includeGroup("org.jetbrains.dokka.dokkatoo-html") + | } + | } + | } + |} + | + |pluginManagement { + | repositories { + | mavenCentral() + | gradlePluginPortal() + | maven(file("$testMavenRepoRelativePath")) { + | mavenContent { + | includeGroup("org.jetbrains.dokka.dokkatoo") + | includeGroup("org.jetbrains.dokka.dokkatoo-html") + | } + | } + | } + |} + | + """.trimMargin() + + gradleProperties = """ + |kotlin.mpp.stability.nowarn=true + |org.gradle.cache=true + """.trimMargin() + + build() + } +} + +/** + * Builder for testing a Gradle project that uses Groovy script and creates default, + * `settings.gradle`, and `gradle.properties` files. + * + * @param[testProjectName] the name of the test, which should be distinct across the project + */ +fun gradleGroovyProjectTest( + testProjectName: String, + baseDir: Path = GradleProjectTest.funcTestTempDir, + build: GradleProjectTest.() -> Unit, +): GradleProjectTest { + return GradleProjectTest(baseDir = baseDir, testProjectName = testProjectName).apply { + + settingsGradle = """ + |rootProject.name = "test" + | + |dependencyResolutionManagement { + | repositories { + | mavenCentral() + | maven { url = file("$testMavenRepoRelativePath") } + | } + |} + | + |pluginManagement { + | repositories { + | mavenCentral() + | gradlePluginPortal() + | maven { url = file("$testMavenRepoRelativePath") } + | } + |} + | + """.trimMargin() + + gradleProperties = """ + |kotlin.mpp.stability.nowarn=true + |org.gradle.cache=true + """.trimMargin() + + build() + } +} + + +fun GradleProjectTest.projectFile( + @Language("TEXT") + filePath: String +): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, String>> = + PropertyDelegateProvider { _, _ -> + TestProjectFileProvidedDelegate(this, filePath) + } + + +/** Delegate for reading and writing a [GradleProjectTest] file. */ +private class TestProjectFileProvidedDelegate( + private val project: GradleProjectTest, + private val filePath: String, +) : ReadWriteProperty<Any?, String> { + override fun getValue(thisRef: Any?, property: KProperty<*>): String = + project.projectDir.resolve(filePath).toFile().readText() + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + project.createFile(filePath, value) + } +} + +/** Delegate for reading and writing a [GradleProjectTest] file. */ +class TestProjectFileDelegate( + private val filePath: String, +) : ReadWriteProperty<ProjectDirectoryScope, String> { + override fun getValue(thisRef: ProjectDirectoryScope, property: KProperty<*>): String = + thisRef.projectDir.resolve(filePath).toFile().readText() + + override fun setValue(thisRef: ProjectDirectoryScope, property: KProperty<*>, value: String) { + thisRef.createFile(filePath, value) + } +} + + +@DslMarker +annotation class ProjectDirectoryDsl + +@ProjectDirectoryDsl +interface ProjectDirectoryScope { + val projectDir: Path +} + +private data class ProjectDirectoryScopeImpl( + override val projectDir: Path +) : ProjectDirectoryScope + + +fun ProjectDirectoryScope.createFile(filePath: String, contents: String): File = + projectDir.resolve(filePath).toFile().apply { + parentFile.mkdirs() + createNewFile() + writeText(contents) + } + + +@ProjectDirectoryDsl +fun ProjectDirectoryScope.dir( + path: String, + block: ProjectDirectoryScope.() -> Unit = {}, +): ProjectDirectoryScope = + ProjectDirectoryScopeImpl(projectDir.resolve(path)).apply(block) + + +@ProjectDirectoryDsl +fun ProjectDirectoryScope.file( + path: String +): Path = projectDir.resolve(path) + + +fun ProjectDirectoryScope.findFiles(matcher: (File) -> Boolean): Sequence<File> = + projectDir.toFile().walk().filter(matcher) + + +/** Set the content of `settings.gradle.kts` */ +@delegate:Language("kts") +var ProjectDirectoryScope.settingsGradleKts: String by TestProjectFileDelegate("settings.gradle.kts") + + +/** Set the content of `build.gradle.kts` */ +@delegate:Language("kts") +var ProjectDirectoryScope.buildGradleKts: String by TestProjectFileDelegate("build.gradle.kts") + + +/** Set the content of `settings.gradle` */ +@delegate:Language("groovy") +var ProjectDirectoryScope.settingsGradle: String by TestProjectFileDelegate("settings.gradle") + + +/** Set the content of `build.gradle` */ +@delegate:Language("groovy") +var ProjectDirectoryScope.buildGradle: String by TestProjectFileDelegate("build.gradle") + + +/** Set the content of `gradle.properties` */ +@delegate:Language("properties") +var ProjectDirectoryScope.gradleProperties: String by TestProjectFileDelegate( + /* language=text */ "gradle.properties" +) + + +fun ProjectDirectoryScope.createKotlinFile(filePath: String, @Language("kotlin") contents: String) = + createFile(filePath, contents) + + +fun ProjectDirectoryScope.createKtsFile(filePath: String, @Language("kts") contents: String) = + createFile(filePath, contents) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt new file mode 100644 index 00000000..d6eadba0 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/KotestProjectConfig.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.core.config.AbstractProjectConfig + +@Suppress("unused") // this class is automatically picked up by Kotest +object KotestProjectConfig : AbstractProjectConfig() { + init { + displayFullTestPath = true + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt new file mode 100644 index 00000000..4ba850d3 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/fileTree.kt @@ -0,0 +1,61 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File +import java.nio.file.Path + +// based on https://gist.github.com/mfwgenerics/d1ec89eb80c95da9d542a03b49b5e15b +// context: https://kotlinlang.slack.com/archives/C0B8MA7FA/p1676106647658099 + +fun Path.toTreeString(): String = toFile().toTreeString() + +fun File.toTreeString(): String = when { + isDirectory -> name + "/\n" + buildTreeString(this) + else -> name +} + +private fun buildTreeString( + dir: File, + margin: String = "", +): String { + val entries = dir.listDirectoryEntries() + + return entries.joinToString("\n") { entry -> + val (currentPrefix, nextPrefix) = when (entry) { + entries.last() -> PrefixPair.LAST_ENTRY + else -> PrefixPair.INTERMEDIATE + } + + buildString { + append("$margin${currentPrefix}${entry.name}") + + if (entry.isDirectory) { + append("/") + if (entry.countDirectoryEntries() > 0) { + append("\n") + } + append(buildTreeString(entry, margin + nextPrefix)) + } + } + } +} + +private fun File.listDirectoryEntries(): Sequence<File> = + walkTopDown().maxDepth(1).filter { it != this@listDirectoryEntries } + + +private fun File.countDirectoryEntries(): Int = + listDirectoryEntries().count() + +private data class PrefixPair( + /** The current entry should be prefixed with this */ + val currentPrefix: String, + /** If the next item is a directory, it should be prefixed with this */ + val nextPrefix: String, +) { + companion object { + /** Prefix pair for a non-last directory entry */ + val INTERMEDIATE = PrefixPair("├── ", "│ ") + /** Prefix pair for the last directory entry */ + val LAST_ENTRY = PrefixPair("└── ", " ") + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt new file mode 100644 index 00000000..6a423b55 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/files.kt @@ -0,0 +1,6 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import java.io.File + +fun File.copyInto(directory: File, overwrite: Boolean = false) = + copyTo(directory.resolve(name), overwrite = overwrite) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt new file mode 100644 index 00000000..912d1df1 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/gradleRunnerUtils.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.internal.DefaultGradleRunner + + +/** Edit environment variables in the Gradle Runner */ +@Deprecated("Windows does not support withEnvironment - https://github.com/gradle/gradle/issues/23959") +fun GradleRunner.withEnvironment(build: MutableMap<String, String?>.() -> Unit): GradleRunner { + val env = environment ?: mutableMapOf() + env.build() + return withEnvironment(env) +} + + +inline fun GradleRunner.build( + handleResult: BuildResult.() -> Unit +): Unit = build().let(handleResult) + + +inline fun GradleRunner.buildAndFail( + handleResult: BuildResult.() -> Unit +): Unit = buildAndFail().let(handleResult) + + +fun GradleRunner.withJvmArguments( + vararg jvmArguments: String +): GradleRunner = (this as DefaultGradleRunner).withJvmArguments(*jvmArguments) + + +/** + * Helper function to _append_ [arguments] to any existing + * [GradleRunner arguments][GradleRunner.getArguments]. + */ +fun GradleRunner.addArguments( + vararg arguments: String +): GradleRunner = + withArguments(this@addArguments.arguments + arguments) + + +/** + * Get the name of the task, without the leading [BuildTask.getPath]. + */ +val BuildTask.name: String + get() = path.substringAfterLast(':') diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt new file mode 100644 index 00000000..8c33e3eb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestCollectionMatchers.kt @@ -0,0 +1,20 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.maps.shouldContainAll +import io.kotest.matchers.maps.shouldContainExactly + +/** @see io.kotest.matchers.maps.shouldContainAll */ +fun <K, V> Map<K, V>.shouldContainAll( + vararg expected: Pair<K, V> +): Unit = shouldContainAll(expected.toMap()) + +/** @see io.kotest.matchers.maps.shouldContainExactly */ +fun <K, V> Map<K, V>.shouldContainExactly( + vararg expected: Pair<K, V> +): Unit = shouldContainExactly(expected.toMap()) + +/** Verify the sequence contains a single element, matching [match]. */ +fun <T> Sequence<T>.shouldBeSingleton(match: (T) -> Unit) { + toList().shouldBeSingleton(match) +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt new file mode 100644 index 00000000..7b692afb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestConditions.kt @@ -0,0 +1,10 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.core.annotation.EnabledCondition +import io.kotest.core.spec.Spec +import kotlin.reflect.KClass + +class NotWindowsCondition : EnabledCondition { + override fun enabled(kclass: KClass<out Spec>): Boolean = + "win" !in System.getProperty("os.name").lowercase() +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt new file mode 100644 index 00000000..e1863c8f --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestGradleAssertions.kt @@ -0,0 +1,130 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.* +import org.gradle.api.NamedDomainObjectCollection +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.TaskOutcome + +infix fun <T : Any> NamedDomainObjectCollection<out T>?.shouldContainDomainObject( + name: String +): T { + this should containDomainObject(name) + return this?.getByName(name)!! +} + +infix fun <T : Any> NamedDomainObjectCollection<out T>?.shouldNotContainDomainObject( + name: String +): NamedDomainObjectCollection<out T>? { + this shouldNot containDomainObject(name) + return this +} + +private fun <T> containDomainObject(name: String): Matcher<NamedDomainObjectCollection<T>?> = + neverNullMatcher { value -> + MatcherResult( + name in value.names, + { "NamedDomainObjectCollection(${value.names}) should contain DomainObject named '$name'" }, + { "NamedDomainObjectCollection(${value.names}) should not contain DomainObject named '$name'" }) + } + +/** Assert that a task ran. */ +infix fun BuildResult?.shouldHaveRunTask(taskPath: String): BuildTask { + this should haveTask(taskPath) + return this?.task(taskPath)!! +} + +/** Assert that a task ran, with an [expected outcome][expectedOutcome]. */ +fun BuildResult?.shouldHaveRunTask( + taskPath: String, + expectedOutcome: TaskOutcome +): BuildTask { + this should haveTask(taskPath) + val task = this?.task(taskPath)!! + task should haveOutcome(expectedOutcome) + return task +} + +/** + * Assert that a task did not run. + * + * A task might not have run if one of its dependencies failed before it could be run. + */ +infix fun BuildResult?.shouldNotHaveRunTask(taskPath: String) { + this shouldNot haveTask(taskPath) +} + +private fun haveTask(taskPath: String): Matcher<BuildResult?> = + neverNullMatcher { value -> + MatcherResult( + value.task(taskPath) != null, + { "BuildResult should have run task $taskPath. All tasks: ${value.tasks.joinToString { it.path }}" }, + { "BuildResult should not have run task $taskPath. All tasks: ${value.tasks.joinToString { it.path }}" }, + ) + } + + +infix fun BuildTask?.shouldHaveOutcome(outcome: TaskOutcome) { + this should haveOutcome(outcome) +} + + +infix fun BuildTask?.shouldHaveAnyOutcome(outcomes: Collection<TaskOutcome>) { + this should haveAnyOutcome(outcomes) +} + + +infix fun BuildTask?.shouldNotHaveOutcome(outcome: TaskOutcome) { + this shouldNot haveOutcome(outcome) +} + + +private fun haveOutcome(outcome: TaskOutcome): Matcher<BuildTask?> = + haveAnyOutcome(listOf(outcome)) + + +private fun haveAnyOutcome(outcomes: Collection<TaskOutcome>): Matcher<BuildTask?> { + val shouldHaveOutcome = when (outcomes.size) { + 0 -> error("Must provide 1 or more expected task outcome, but received none") + 1 -> "should have outcome ${outcomes.first().name}" + else -> "should have any outcome of ${outcomes.joinToString()}" + } + + return neverNullMatcher { value -> + MatcherResult( + value.outcome in outcomes, + { "Task ${value.path} $shouldHaveOutcome, but was ${value.outcome}" }, + { "Task ${value.path} $shouldHaveOutcome, but was ${value.outcome}" }, + ) + } +} + +fun BuildResult.shouldHaveTaskWithOutcome(taskPath: String, outcome: TaskOutcome) { + this shouldHaveRunTask taskPath shouldHaveOutcome outcome +} + + +fun BuildResult.shouldHaveTaskWithAnyOutcome(taskPath: String, outcomes: Collection<TaskOutcome>) { + this shouldHaveRunTask taskPath shouldHaveAnyOutcome outcomes +} + +fun BuildResult.shouldHaveTasksWithOutcome( + vararg taskPathToExpectedOutcome: Pair<String, TaskOutcome> +) { + assertSoftly { + taskPathToExpectedOutcome.forEach { (taskPath, outcome) -> + shouldHaveTaskWithOutcome(taskPath, outcome) + } + } +} + +fun BuildResult.shouldHaveTasksWithAnyOutcome( + vararg taskPathToExpectedOutcome: Pair<String, Collection<TaskOutcome>> +) { + assertSoftly { + taskPathToExpectedOutcome.forEach { (taskPath, outcomes) -> + shouldHaveTaskWithAnyOutcome(taskPath, outcomes) + } + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt new file mode 100644 index 00000000..58bbe768 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/kotestStringMatchers.kt @@ -0,0 +1,65 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import io.kotest.assertions.print.print +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.neverNullMatcher +import io.kotest.matchers.should +import io.kotest.matchers.shouldNot + + +infix fun String?.shouldContainAll(substrings: Iterable<String>): String? { + this should containAll(substrings) + return this +} + +infix fun String?.shouldNotContainAll(substrings: Iterable<String>): String? { + this shouldNot containAll(substrings) + return this +} + +fun String?.shouldContainAll(vararg substrings: String): String? { + this should containAll(substrings.asList()) + return this +} + +fun String?.shouldNotContainAll(vararg substrings: String): String? { + this shouldNot containAll(substrings.asList()) + return this +} + +private fun containAll(substrings: Iterable<String>) = + neverNullMatcher<String> { value -> + MatcherResult( + substrings.all { it in value }, + { "${value.print().value} should include substrings ${substrings.print().value}" }, + { "${value.print().value} should not include substrings ${substrings.print().value}" }) + } + + +infix fun String?.shouldContainAnyOf(substrings: Iterable<String>): String? { + this should containAnyOf(substrings) + return this +} + +infix fun String?.shouldNotContainAnyOf(substrings: Iterable<String>): String? { + this shouldNot containAnyOf(substrings) + return this +} + +fun String?.shouldContainAnyOf(vararg substrings: String): String? { + this should containAnyOf(substrings.asList()) + return this +} + +fun String?.shouldNotContainAnyOf(vararg substrings: String): String? { + this shouldNot containAnyOf(substrings.asList()) + return this +} + +private fun containAnyOf(substrings: Iterable<String>) = + neverNullMatcher<String> { value -> + MatcherResult( + substrings.any { it in value }, + { "${value.print().value} should include any of these substrings ${substrings.print().value}" }, + { "${value.print().value} should not include any of these substrings ${substrings.print().value}" }) + } diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt new file mode 100644 index 00000000..62cd5860 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/samWithReceiverWorkarounds.kt @@ -0,0 +1,77 @@ +@file:Suppress("FunctionName") + +package org.jetbrains.dokka.dokkatoo.utils + +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaPackageOptionsSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceLinkSpec +import org.jetbrains.dokka.dokkatoo.dokka.parameters.DokkaSourceSetSpec +import org.gradle.api.DomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.NamedDomainObjectProvider +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.DependencySet + + +/** + * Workarounds because `SamWithReceiver` not working in test sources + * https://youtrack.jetbrains.com/issue/KTIJ-14684 + * + * The `SamWithReceiver` plugin is automatically applied by the `kotlin-dsl` plugin. + * It converts all [org.gradle.api.Action] so the parameter is the receiver: + * + * ``` + * // with SamWithReceiver ✅ + * tasks.configureEach { + * val task: Task = this + * } + * + * // without SamWithReceiver + * tasks.configureEach { it -> + * val task: Task = it + * } + * ``` + * + * This is nice because it means that the Dokka Gradle Plugin more closely matches `build.gradle.kts` files. + * + * However, [IntelliJ is bugged](https://youtrack.jetbrains.com/issue/KTIJ-14684) and doesn't + * acknowledge that `SamWithReceiver` has been applied in test sources. The code works and compiles, + * but IntelliJ shows red errors. + * + * These functions are workarounds, and should be removed ASAP. + */ +@Suppress("unused") +private object Explain + +fun Project.subprojects_(configure: Project.() -> Unit) = + subprojects(configure) + +@Suppress("SpellCheckingInspection") +fun Project.allprojects_(configure: Project.() -> Unit) = + allprojects(configure) + +fun <T> DomainObjectCollection<T>.configureEach_(configure: T.() -> Unit) = + configureEach(configure) + +fun <T> DomainObjectCollection<T>.all_(configure: T.() -> Unit) = + all(configure) + +fun Configuration.withDependencies_(action: DependencySet.() -> Unit): Configuration = + withDependencies(action) + +fun <T> NamedDomainObjectContainer<T>.create_(name: String, configure: T.() -> Unit = {}): T = + create(name, configure) + +fun <T> NamedDomainObjectContainer<T>.register_( + name: String, + configure: T.() -> Unit +): NamedDomainObjectProvider<T> = + register(name, configure) + +fun DokkaSourceSetSpec.sourceLink_( + action: DokkaSourceLinkSpec.() -> Unit +): Unit = sourceLink(action) + +fun DokkaSourceSetSpec.perPackageOption_( + action: DokkaPackageOptionsSpec.() -> Unit +): Unit = perPackageOption(action) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt new file mode 100644 index 00000000..eb8777e7 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/stringUtils.kt @@ -0,0 +1,21 @@ +package org.jetbrains.dokka.dokkatoo.utils + + +fun String.splitToPair(delimiter: String): Pair<String, String> = + substringBefore(delimiter) to substringAfter(delimiter) + + +/** Title case the first char of a string */ +fun String.uppercaseFirstChar(): String = mapFirstChar(Character::toTitleCase) + + +private inline fun String.mapFirstChar( + transform: (Char) -> Char +): String = if (isNotEmpty()) transform(this[0]) + substring(1) else this + + +/** Split a string into lines, sort the lines, and re-join them (using [separator]). */ +fun String.sortLines(separator: String = "\n") = + lines() + .sorted() + .joinToString(separator) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt new file mode 100644 index 00000000..b15b3edb --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/systemVariableProviders.kt @@ -0,0 +1,40 @@ +package org.jetbrains.dokka.dokkatoo.utils + +import kotlin.properties.ReadOnlyProperty + +// Utilities for fetching System Properties and Environment Variables via delegated properties + + +internal fun optionalSystemProperty() = optionalSystemProperty { it } + +internal fun <T : Any> optionalSystemProperty( + convert: (String) -> T? +): ReadOnlyProperty<Any, T?> = + ReadOnlyProperty { _, property -> + val value = System.getProperty(property.name) + if (value != null) convert(value) else null + } + + +internal fun systemProperty() = systemProperty { it } + +internal fun <T> systemProperty( + convert: (String) -> T +): ReadOnlyProperty<Any, T> = + ReadOnlyProperty { _, property -> + val value = requireNotNull(System.getProperty(property.name)) { + "system property ${property.name} is unavailable" + } + convert(value) + } + + +internal fun optionalEnvironmentVariable() = optionalEnvironmentVariable { it } + +internal fun <T : Any> optionalEnvironmentVariable( + convert: (String) -> T? +): ReadOnlyProperty<Any, T?> = + ReadOnlyProperty { _, property -> + val value = System.getenv(property.name) + if (value != null) convert(value) else null + } diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt new file mode 100644 index 00000000..ce0ebd9d --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFixtures/kotlin/text.kt @@ -0,0 +1,24 @@ +package org.jetbrains.dokka.dokkatoo.utils + +/** Replace all newlines with `\n`, so the String can be used in assertions cross-platform */ +fun String.invariantNewlines(): String = + lines().joinToString("\n") + +fun Pair<String, String>.sideBySide( + buffer: String = " ", +): String { + val (left, right) = this + + val leftLines = left.lines() + val rightLines = right.lines() + + val maxLeftWidth = leftLines.maxOf { it.length } + + return (0..maxOf(leftLines.size, rightLines.size)).joinToString("\n") { i -> + + val leftLine = (leftLines.getOrNull(i) ?: "").padEnd(maxLeftWidth, ' ') + val rightLine = rightLines.getOrNull(i) ?: "" + + leftLine + buffer + rightLine + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt new file mode 100644 index 00000000..90d587ce --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/DokkatooPluginFunctionalTest.kt @@ -0,0 +1,205 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooConstants.DOKKATOO_VERSION +import org.jetbrains.dokka.dokkatoo.utils.* +import io.kotest.assertions.asClue +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.string.shouldContain + +class DokkatooPluginFunctionalTest : FunSpec({ + val testProject = gradleKtsProjectTest("DokkatooPluginFunctionalTest") { + buildGradleKts = """ + |plugins { + | id("org.jetbrains.dokka.dokkatoo") version "$DOKKATOO_VERSION" + |} + | + """.trimMargin() + } + + test("expect Dokka Plugin creates Dokka tasks") { + testProject.runner + .addArguments("tasks", "--group=dokkatoo", "-q") + .build { + withClue(output) { + val dokkatooTasks = output + .substringAfter("Dokkatoo tasks") + .lines() + .filter { it.contains(" - ") } + .associate { it.splitToPair(" - ") } + + dokkatooTasks.shouldContainExactly( + //@formatter:off + "dokkatooGenerate" to "Generates Dokkatoo publications for all formats", + "dokkatooGenerateModuleGfm" to "Executes the Dokka Generator, generating a gfm module", + "dokkatooGenerateModuleHtml" to "Executes the Dokka Generator, generating a html module", + "dokkatooGenerateModuleJavadoc" to "Executes the Dokka Generator, generating a javadoc module", + "dokkatooGenerateModuleJekyll" to "Executes the Dokka Generator, generating a jekyll module", + "dokkatooGeneratePublicationGfm" to "Executes the Dokka Generator, generating the gfm publication", + "dokkatooGeneratePublicationHtml" to "Executes the Dokka Generator, generating the html publication", + "dokkatooGeneratePublicationJavadoc" to "Executes the Dokka Generator, generating the javadoc publication", + "dokkatooGeneratePublicationJekyll" to "Executes the Dokka Generator, generating the jekyll publication", + "prepareDokkatooModuleDescriptorGfm" to "Prepares the Dokka Module Descriptor for gfm", + "prepareDokkatooModuleDescriptorHtml" to "Prepares the Dokka Module Descriptor for html", + "prepareDokkatooModuleDescriptorJavadoc" to "Prepares the Dokka Module Descriptor for javadoc", + "prepareDokkatooModuleDescriptorJekyll" to "Prepares the Dokka Module Descriptor for jekyll", + //@formatter:on + ) + } + } + } + + test("expect Dokka Plugin creates Dokka outgoing variants") { + val build = testProject.runner + .addArguments("outgoingVariants", "-q") + .build { + val variants = output.invariantNewlines().replace('\\', '/') + + val dokkatooVariants = variants.lines() + .filter { it.contains("dokka", ignoreCase = true) } + .mapNotNull { it.substringAfter("Variant ", "").takeIf(String::isNotBlank) } + + + dokkatooVariants.shouldContainExactlyInAnyOrder( + "dokkatooModuleElementsGfm", + "dokkatooModuleElementsHtml", + "dokkatooModuleElementsJavadoc", + "dokkatooModuleElementsJekyll", + ) + + fun checkVariant(format: String) { + val formatCapitalized = format.uppercaseFirstChar() + + variants shouldContain /* language=text */ """ + |-------------------------------------------------- + |Variant dokkatooModuleElements$formatCapitalized + |-------------------------------------------------- + |Provide Dokka Module files for $format to other subprojects + | + |Capabilities + | - :test:unspecified (default capability) + |Attributes + | - org.jetbrains.dokka.dokkatoo.base = dokkatoo + | - org.jetbrains.dokka.dokkatoo.category = module-files + | - org.jetbrains.dokka.dokkatoo.format = $format + |Artifacts + | - build/dokka-config/$format/module_descriptor.json (artifactType = json) + | - build/dokka-module/$format (artifactType = directory) + | + """.trimMargin() + } + + checkVariant("gfm") + checkVariant("html") + checkVariant("javadoc") + checkVariant("jekyll") + } + } + + test("expect Dokka Plugin creates Dokka resolvable configurations") { + + val expectedFormats = listOf("Gfm", "Html", "Javadoc", "Jekyll") + + testProject.runner + .addArguments("resolvableConfigurations", "-q") + .build { + output.invariantNewlines().asClue { allConfigurations -> + + val dokkatooConfigurations = allConfigurations.lines() + .filter { it.contains("dokka", ignoreCase = true) } + .mapNotNull { it.substringAfter("Configuration ", "").takeIf(String::isNotBlank) } + + dokkatooConfigurations.shouldContainExactlyInAnyOrder( + buildList { + add("dokkatoo") + + addAll(expectedFormats.map { "dokkatooModule$it" }) + addAll(expectedFormats.map { "dokkatooGeneratorClasspath$it" }) + addAll(expectedFormats.map { "dokkatooPlugin$it" }) + addAll(expectedFormats.map { "dokkatooPluginIntransitive$it" }) + } + ) + + withClue("Configuration dokka") { + output.invariantNewlines() shouldContain /* language=text */ """ + |-------------------------------------------------- + |Configuration dokkatoo + |-------------------------------------------------- + |Fetch all Dokkatoo files from all configurations in other subprojects + | + |Attributes + | - org.jetbrains.dokka.dokkatoo.base = dokkatoo + | + """.trimMargin() + } + + fun checkConfigurations(format: String) { + val formatLowercase = format.lowercase() + + allConfigurations shouldContain /* language=text */ """ + |-------------------------------------------------- + |Configuration dokkatooGeneratorClasspath$format + |-------------------------------------------------- + |Dokka Generator runtime classpath for $formatLowercase - will be used in Dokka Worker. Should contain all transitive dependencies, plugins (and their transitive dependencies), so Dokka Worker can run. + | + |Attributes + | - org.jetbrains.dokka.dokkatoo.base = dokkatoo + | - org.jetbrains.dokka.dokkatoo.category = generator-classpath + | - org.jetbrains.dokka.dokkatoo.format = $formatLowercase + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = java-runtime + |Extended Configurations + | - dokkatooPlugin$format + | + """.trimMargin() + + allConfigurations shouldContain /* language=text */ """ + |-------------------------------------------------- + |Configuration dokkatooPlugin$format + |-------------------------------------------------- + |Dokka Plugins classpath for $formatLowercase + | + |Attributes + | - org.jetbrains.dokka.dokkatoo.base = dokkatoo + | - org.jetbrains.dokka.dokkatoo.category = plugins-classpath + | - org.jetbrains.dokka.dokkatoo.format = $formatLowercase + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = java-runtime + | + """.trimMargin() + + allConfigurations shouldContain /* language=text */ """ + |-------------------------------------------------- + |Configuration dokkatooPluginIntransitive$format + |-------------------------------------------------- + |Dokka Plugins classpath for $formatLowercase - for internal use. Fetch only the plugins (no transitive dependencies) for use in the Dokka JSON Configuration. + | + |Attributes + | - org.jetbrains.dokka.dokkatoo.base = dokkatoo + | - org.jetbrains.dokka.dokkatoo.category = plugins-classpath + | - org.jetbrains.dokka.dokkatoo.format = $formatLowercase + | - org.gradle.category = library + | - org.gradle.dependency.bundling = external + | - org.gradle.jvm.environment = standard-jvm + | - org.gradle.libraryelements = jar + | - org.gradle.usage = java-runtime + |Extended Configurations + | - dokkatooPlugin$format + | + """.trimMargin() + } + + expectedFormats.forEach { + checkConfigurations(it) + } + } + } + } +}) diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/GradlePluginProjectIntegrationTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/GradlePluginProjectIntegrationTest.kt new file mode 100644 index 00000000..d35150a2 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/GradlePluginProjectIntegrationTest.kt @@ -0,0 +1,110 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooConstants +import org.jetbrains.dokka.dokkatoo.utils.* +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.sequences.shouldNotBeEmpty +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain + +class GradlePluginProjectIntegrationTest : FunSpec({ + + context("given a gradle plugin project") { + val project = initGradlePluginProject() + + project.runner + .addArguments( + "clean", + "dokkatooGeneratePublicationHtml", + "--stacktrace", + ) + .forwardOutput() + .build { + + test("expect project builds successfully") { + output shouldContain "BUILD SUCCESSFUL" + } + + test("expect no 'unknown class' message in HTML files") { + val htmlFiles = project.projectDir.toFile() + .resolve("build/dokka/html") + .walk() + .filter { it.isFile && it.extension == "html" } + + htmlFiles.shouldNotBeEmpty() + + htmlFiles.forEach { htmlFile -> + val relativePath = htmlFile.relativeTo(project.projectDir.toFile()) + withClue("$relativePath should not contain Error class: unknown class") { + htmlFile.useLines { lines -> + lines.shouldForAll { line -> line.shouldNotContain("Error class: unknown class") } + } + } + } + } + } + } +}) + +private fun initGradlePluginProject( + config: GradleProjectTest.() -> Unit = {}, +): GradleProjectTest { + return gradleKtsProjectTest("gradle-plugin-project") { + + settingsGradleKts += """ + | + """.trimMargin() + + buildGradleKts = """ + |plugins { + | `kotlin-dsl` + | id("org.jetbrains.dokka.dokkatoo") version "${DokkatooConstants.DOKKATOO_VERSION}" + |} + | + """.trimMargin() + + dir("src/main/kotlin") { + + createKotlinFile( + "MyCustomGradlePlugin.kt", + """ + |package com.project.gradle.plugin + | + |import javax.inject.Inject + |import org.gradle.api.Plugin + |import org.gradle.api.Project + |import org.gradle.api.model.ObjectFactory + |import org.gradle.kotlin.dsl.* + | + |abstract class MyCustomGradlePlugin @Inject constructor( + | private val objects: ObjectFactory + |) : Plugin<Project> { + | override fun apply(project: Project) { + | println(objects.property<String>().getOrElse("empty")) + | } + |} + + """.trimMargin() + ) + + createKotlinFile( + "MyCustomGradlePluginExtension.kt", + """ + |package com.project.gradle.plugin + | + |import org.gradle.api.provider.* + | + |interface MyCustomGradlePluginExtension { + | val versionProperty: Property<String> + | val versionProvider: Provider<String> + |} + | + """.trimMargin() + ) + } + + config() + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/KotlinMultiplatformFunctionalTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/KotlinMultiplatformFunctionalTest.kt new file mode 100644 index 00000000..23a6744c --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/KotlinMultiplatformFunctionalTest.kt @@ -0,0 +1,247 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooConstants +import org.jetbrains.dokka.dokkatoo.utils.* +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.file.shouldBeAFile +import io.kotest.matchers.paths.shouldBeAFile +import io.kotest.matchers.sequences.shouldNotBeEmpty +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain + +class KotlinMultiplatformFunctionalTest : FunSpec({ + + context("when dokkatoo generates all formats") { + val project = initKotlinMultiplatformProject() + + project.runner + .addArguments( + "clean", + ":dokkatooGeneratePublicationHtml", + "--stacktrace", + ) + .forwardOutput() + .build { + test("expect build is successful") { + output shouldContain "BUILD SUCCESSFUL" + } + } + + test("expect all dokka workers are successful") { + project + .findFiles { it.name == "dokka-worker.log" } + .shouldBeSingleton { dokkaWorkerLog -> + dokkaWorkerLog.shouldBeAFile() + dokkaWorkerLog.readText().shouldNotContainAnyOf( + "[ERROR]", + "[WARN]", + ) + } + } + + context("expect HTML site is generated") { + + test("with expected HTML files") { + project.projectDir.resolve("build/dokka/html/index.html").shouldBeAFile() + project.projectDir.resolve("build/dokka/html/com/project/hello/Hello.html") + .shouldBeAFile() + } + + test("and dokka_parameters.json is generated") { + project.projectDir.resolve("build/dokka/html/dokka_parameters.json") + .shouldBeAFile() + } + + test("with element-list") { + project.projectDir.resolve("build/dokka/html/test/package-list").shouldBeAFile() + project.projectDir.resolve("build/dokka/html/test/package-list").toFile().readText() + .sortLines() + .shouldContain( /* language=text */ """ + |${'$'}dokka.format:html-v1 + |${'$'}dokka.linkExtension:html + |${'$'}dokka.location:com.project////PointingToDeclaration/test/com.project/index.html + |${'$'}dokka.location:com.project//goodbye/#kotlinx.serialization.json.JsonObject/PointingToDeclaration/test/com.project/goodbye.html + |${'$'}dokka.location:com.project/Hello///PointingToDeclaration/test/com.project/-hello/index.html + |${'$'}dokka.location:com.project/Hello/Hello/#/PointingToDeclaration/test/com.project/-hello/-hello.html + |${'$'}dokka.location:com.project/Hello/sayHello/#kotlinx.serialization.json.JsonObject/PointingToDeclaration/test/com.project/-hello/say-hello.html + |com.project + """.trimMargin() + ) + } + + test("expect no 'unknown class' message in HTML files") { + val htmlFiles = project.projectDir.toFile() + .resolve("build/dokka/html") + .walk() + .filter { it.isFile && it.extension == "html" } + + htmlFiles.shouldNotBeEmpty() + + htmlFiles.forEach { htmlFile -> + val relativePath = htmlFile.relativeTo(project.projectDir.toFile()) + withClue("$relativePath should not contain Error class: unknown class") { + htmlFile.useLines { lines -> + lines.shouldForAll { line -> line.shouldNotContain("Error class: unknown class") } + } + } + } + } + } + } +}) + + +private fun initKotlinMultiplatformProject( + config: GradleProjectTest.() -> Unit = {}, +): GradleProjectTest { + return gradleKtsProjectTest("kotlin-multiplatform-project") { + + settingsGradleKts += """ + | + |dependencyResolutionManagement { + | + | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + | + | repositories { + | mavenCentral() + | + | // Declare the Node.js & Yarn download repositories + | exclusiveContent { + | forRepository { + | ivy("https://nodejs.org/dist/") { + | name = "Node Distributions at ${'$'}url" + | patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") } + | metadataSources { artifact() } + | content { includeModule("org.nodejs", "node") } + | } + | } + | filter { includeGroup("org.nodejs") } + | } + | + | exclusiveContent { + | forRepository { + | ivy("https://github.com/yarnpkg/yarn/releases/download") { + | name = "Node Distributions at ${'$'}url" + | patternLayout { artifact("v[revision]/[artifact](-v[revision]).[ext]") } + | metadataSources { artifact() } + | content { includeModule("com.yarnpkg", "yarn") } + | } + | } + | filter { includeGroup("com.yarnpkg") } + | } + | } + |} + | + """.trimMargin() + + buildGradleKts = """ + |plugins { + | kotlin("multiplatform") version "1.8.22" + | id("org.jetbrains.dokka.dokkatoo") version "${DokkatooConstants.DOKKATOO_VERSION}" + |} + | + |kotlin { + | jvm() + | js(IR) { + | browser() + | } + | + | sourceSets { + | commonMain { + | dependencies { + | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") + | } + | } + | commonTest { + | dependencies { + | implementation(kotlin("test")) + | } + | } + | } + |} + | + |dependencies { + | // must manually add this dependency for aggregation to work + | //dokkatooPluginHtml("org.jetbrains.dokka:all-modules-page-plugin:1.8.10") + |} + | + |dokkatoo { + | dokkatooSourceSets.configureEach { + | externalDocumentationLinks { + | create("kotlinxSerialization") { + | url("https://kotlinlang.org/api/kotlinx.serialization/") + | } + | } + | } + |} + | + | + """.trimMargin() + + dir("src/commonMain/kotlin/") { + + createKotlinFile( + "Hello.kt", + """ + |package com.project + | + |import kotlinx.serialization.json.JsonObject + | + |/** The Hello class */ + |class Hello { + | /** prints `Hello` and [json] to the console */ + | fun sayHello(json: JsonObject) = println("Hello ${'$'}json") + |} + | + """.trimMargin() + ) + + createKotlinFile( + "goodbye.kt", + """ + |package com.project + | + |import kotlinx.serialization.json.JsonObject + | + |/** Should print `goodbye` and [json] to the console */ + |expect fun goodbye(json: JsonObject) + | + """.trimMargin() + ) + } + + dir("src/jvmMain/kotlin/") { + createKotlinFile( + "goodbyeJvm.kt", + """ + |package com.project + | + |import kotlinx.serialization.json.JsonObject + | + |/** JVM implementation - prints `goodbye` and [json] to the console */ + |actual fun goodbye(json: JsonObject) = println("[JVM] goodbye ${'$'}json") + | + """.trimMargin() + ) + } + + dir("src/jsMain/kotlin/") { + createKotlinFile( + "goodbyeJs.kt", + """ + |package com.project + | + |import kotlinx.serialization.json.JsonObject + | + |/** JS implementation - prints `goodbye` and [json] to the console */ + |actual fun goodbye(json: JsonObject) = println("[JS] goodbye ${'$'}json") + | + """.trimMargin() + ) + } + + config() + } +} diff --git a/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt new file mode 100644 index 00000000..cac20f69 --- /dev/null +++ b/dokka-runners/dokkatoo/modules/dokkatoo-plugin/src/testFunctional/kotlin/MultiModuleFunctionalTest.kt @@ -0,0 +1,468 @@ +package org.jetbrains.dokka.dokkatoo + +import org.jetbrains.dokka.dokkatoo.internal.DokkatooConstants.DOKKATOO_VERSION +import org.jetbrains.dokka.dokkatoo.utils.* +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.shouldForAll +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.file.shouldBeAFile +import io.kotest.matchers.paths.shouldBeAFile +import io.kotest.matchers.paths.shouldNotExist +import io.kotest.matchers.string.shouldBeEmpty +import io.kotest.matchers.string.shouldContain +import org.gradle.testkit.runner.TaskOutcome.* + +class MultiModuleFunctionalTest : FunSpec({ + + context("when dokkatoo generates all formats") { + val project = initDokkatooProject("all-formats") + + project.runner + .addArguments( + "clean", + ":dokkatooGenerate", + "--stacktrace", + ) + .forwardOutput() + .build { + test("expect build is successful") { + output shouldContain "BUILD SUCCESSFUL" + } + } + + test("expect all dokka workers are successful") { + project + .findFiles { it.name == "dokka-worker.log" } + .shouldForAll { dokkaWorkerLog -> + dokkaWorkerLog.shouldBeAFile() + dokkaWorkerLog.readText().shouldNotContainAnyOf( + "[ERROR]", + "[WARN]", + ) + } + } + + context("expect HTML site is generated") { + + test("with expected HTML files") { + project.file("subproject/build/dokka/html/index.html").shouldBeAFile() + project.file("subproject/build/dokka/html/com/project/hello/Hello.html") + .shouldBeAFile() + } + + test("and dokka_parameters.json is generated") { + project.file("subproject/build/dokka/html/dokka_parameters.json") + .shouldBeAFile() + } + + test("with element-list") { + project.file("build/dokka/html/package-list").shouldBeAFile() + project.file("build/dokka/html/package-list").toFile().readText() + .shouldContain( /* language=text */ """ + |${'$'}dokka.format:html-v1 + |${'$'}dokka.linkExtension:html + | + |module:subproject-hello + |com.project.hello + |module:subproject-goodbye + |com.project.goodbye + """.trimMargin() + ) + } + } + } + + context("Gradle caching") { + + context("expect Dokkatoo is compatible with Gradle Build Cache") { + val project = initDokkatooProject("build-cache") + + test("expect clean is successful") { + project.runner.addArguments("clean").build { + output shouldContain "BUILD SUCCESSFUL" + } + } + + project.runner + .addArguments( + //"clean", + ":dokkatooGenerate", + "--stacktrace", + "--build-cache", + ) + .forwardOutput() + .build { + test("expect build is successful") { + output shouldContain "BUILD SUCCESSFUL" + } + + test("expect all dokka workers are successful") { + project + .findFiles { it.name == "dokka-worker.log" } + .shouldForAll { dokkaWorkerLog -> + dokkaWorkerLog.shouldBeAFile() + dokkaWorkerLog.readText().shouldNotContainAnyOf( + "[ERROR]", + "[WARN]", + ) + } + } + } + + context("when build cache is enabled") { + project.runner + .addArguments( + ":dokkatooGenerate", + "--stacktrace", + "--build-cache", + ) + .forwardOutput() + .build { + test("expect build is successful") { + output shouldContainAll listOf( + "BUILD SUCCESSFUL", + "24 actionable tasks: 24 up-to-date", + ) + } + + test("expect all dokkatoo tasks are up-to-date") { + tasks + .filter { task -> + task.name.contains("dokkatoo", ignoreCase = true) + } + .shouldForAll { task -> + task.outcome.shouldBeIn(FROM_CACHE, UP_TO_DATE, SKIPPED) + } + } + } + } + } + + context("Gradle Configuration Cache") { + val project = initDokkatooProject("config-cache") + + test("expect clean is successful") { + project.runner.addArguments("clean").build { + output shouldContain "BUILD SUCCESSFUL" + } + } + + project.runner + .addArguments( + //"clean", + ":dokkatooGenerate", + "--stacktrace", + "--no-build-cache", + "--configuration-cache", + ) + .forwardOutput() + .build { + test("expect build is successful") { + output shouldContain "BUILD SUCCESSFUL" + } + } + + test("expect all dokka workers are successful") { + project + .findFiles { it.name == "dokka-worker.log" } + .shouldForAll { dokkaWorkerLog -> + dokkaWorkerLog.shouldBeAFile() + dokkaWorkerLog.readText().shouldNotContainAnyOf( + "[ERROR]", + "[WARN]", + ) + } + } + } + + + context("expect updates in subprojects re-run tasks") { + + val project = initDokkatooProject("submodule-update") + + test("expect clean is successful") { + project.runner.addArguments("clean").build { + output shouldContain "BUILD SUCCESSFUL" + } + } + + test("expect first build is successful") { + project.runner + .addArguments( + //"clean", + ":dokkatooGeneratePublicationHtml", + "--stacktrace", + "--build-cache", + ) + .forwardOutput() + .build { + output shouldContain "BUILD SUCCESSFUL" + } + } + + context("and when a file in a subproject changes") { + + val helloAgainIndexHtml = + @Suppress("KDocUnresolvedReference") + project.createKotlinFile( + "subproject-hello/src/main/kotlin/HelloAgain.kt", + """ + |package com.project.hello + | + |/** Like [Hello], but again */ + |class HelloAgain { + | /** prints `Hello Again` to the console */ + | fun sayHelloAgain() = println("Hello Again") + |} + | + """.trimMargin() + ).toPath() + + context("expect Dokka re-generates the publication") { + project.runner + .addArguments( + ":dokkatooGeneratePublicationHtml", + "--stacktrace", + "--build-cache", + ) + .forwardOutput() + .build { + + test("expect HelloAgain HTML file exists") { + helloAgainIndexHtml.shouldBeAFile() + } + + test("expect :subproject-goodbye tasks are up-to-date, because no files changed") { + shouldHaveTasksWithOutcome( + ":subproject-goodbye:dokkatooGenerateModuleHtml" to UP_TO_DATE, + ":subproject-goodbye:prepareDokkatooModuleDescriptorHtml" to UP_TO_DATE, + ) + } + + val successfulOutcomes = listOf(SUCCESS, FROM_CACHE) + test("expect :subproject-hello tasks should be re-run, since a file changed") { + shouldHaveTasksWithAnyOutcome( + ":subproject-hello:dokkatooGenerateModuleHtml" to successfulOutcomes, + ":subproject-hello:prepareDokkatooModuleDescriptorHtml" to successfulOutcomes, + ) + } + + test("expect aggregating tasks should re-run because the :subproject-hello Dokka Module changed") { + shouldHaveTasksWithAnyOutcome( + ":dokkatooGeneratePublicationHtml" to successfulOutcomes, + ) + } + + test("expect build is successful") { + output shouldContain "BUILD SUCCESSFUL" + } + + test("expect 5 tasks are run") { + output shouldContain "5 actionable tasks" + } + } + + context("and when the class is deleted") { + project.dir("subproject-hello") { + require(file("src/main/kotlin/HelloAgain.kt").toFile().delete()) { + "failed to delete HelloAgain.kt" + } + } + + project.runner + .addArguments( + ":dokkatooGeneratePublicationHtml", + "--stacktrace", + "--info", + "--build-cache", + ) + .forwardOutput() + .build { + + test("expect HelloAgain HTML file is now deleted") { + helloAgainIndexHtml.shouldNotExist() + + project.dir("build/dokka/html/") { + projectDir.toTreeString().shouldNotContainAnyOf( + "hello-again", + "-hello-again/", + "-hello-again.html", + ) + } + } + } + } + } + } + } + } + + context("logging") { + val project = initDokkatooProject("logging") + + test("expect no logs when built using --quiet log level") { + + project.runner + .addArguments( + "clean", + ":dokkatooGenerate", + "--no-configuration-cache", + "--no-build-cache", + "--quiet", + ) + .forwardOutput() + .build { + output.shouldBeEmpty() + } + } + + test("expect no Dokkatoo logs when built using lifecycle log level") { + + project.runner + .addArguments( + "clean", + ":dokkatooGenerate", + "--no-configuration-cache", + "--no-build-cache", + "--no-parallel", + // no logging option => lifecycle log level + ) + .forwardOutput() + .build { + + // projects are only configured the first time TestKit runs, and annoyingly there's no + // easy way to force Gradle to re-configure the projects - so only check conditionally. + if ("Configure project" in output) { + output shouldContain /*language=text*/ """ + ¦> Configure project : + ¦> Configure project :subproject-goodbye + ¦> Configure project :subproject-hello + ¦> Task :clean + """.trimMargin("¦") + } + + output.lines() + .filter { it.startsWith("> Task :") } + .shouldContainAll( + "> Task :clean", + "> Task :dokkatooGenerate", + "> Task :dokkatooGenerateModuleGfm", + "> Task :dokkatooGenerateModuleHtml", + "> Task :dokkatooGenerateModuleJavadoc", + "> Task :dokkatooGenerateModuleJekyll", + "> Task :dokkatooGeneratePublicationGfm", + "> Task :dokkatooGeneratePublicationHtml", + "> Task :dokkatooGeneratePublicationJavadoc", + "> Task :dokkatooGeneratePublicationJekyll", + "> Task :subproject-goodbye:clean", + "> Task :subproject-goodbye:dokkatooGenerateModuleGfm", + "> Task :subproject-goodbye:dokkatooGenerateModuleHtml", + "> Task :subproject-goodbye:dokkatooGenerateModuleJavadoc", + "> Task :subproject-goodbye:dokkatooGenerateModuleJekyll", + "> Task :subproject-goodbye:prepareDokkatooModuleDescriptorGfm", + "> Task :subproject-goodbye:prepareDokkatooModuleDescriptorHtml", + "> Task :subproject-goodbye:prepareDokkatooModuleDescriptorJavadoc", + "> Task :subproject-goodbye:prepareDokkatooModuleDescriptorJekyll", + "> Task :subproject-hello:clean", + "> Task :subproject-hello:dokkatooGenerateModuleGfm", + "> Task :subproject-hello:dokkatooGenerateModuleHtml", + "> Task :subproject-hello:dokkatooGenerateModuleJavadoc", + "> Task :subproject-hello:dokkatooGenerateModuleJekyll", + "> Task :subproject-hello:prepareDokkatooModuleDescriptorGfm", + "> Task :subproject-hello:prepareDokkatooModuleDescriptorHtml", + "> Task :subproject-hello:prepareDokkatooModuleDescriptorJavadoc", + "> Task :subproject-hello:prepareDokkatooModuleDescriptorJekyll", + ) + } + } + } +}) + +private fun initDokkatooProject( + testName: String, + config: GradleProjectTest.() -> Unit = {}, +): GradleProjectTest { + return gradleKtsProjectTest("multi-module-hello-goodbye/$testName") { + + settingsGradleKts += """ + | + |include(":subproject-hello") + |include(":subproject-goodbye") + | + """.trimMargin() + + buildGradleKts = """ + |plugins { + | // Kotlin plugin shouldn't be necessary here, but without it Dokka errors + | // with ClassNotFound KotlinPluginExtension... very weird + | kotlin("jvm") version "1.8.22" apply false + | id("org.jetbrains.dokka.dokkatoo") version "$DOKKATOO_VERSION" + |} + | + |dependencies { + | dokkatoo(project(":subproject-hello")) + | dokkatoo(project(":subproject-goodbye")) + | dokkatooPluginHtml( + | dokkatoo.versions.jetbrainsDokka.map { dokkaVersion -> + | "org.jetbrains.dokka:all-modules-page-plugin:${'$'}dokkaVersion" + | } + | ) + |} + | + """.trimMargin() + + dir("subproject-hello") { + buildGradleKts = """ + |plugins { + | kotlin("jvm") version "1.8.22" + | id("org.jetbrains.dokka.dokkatoo") version "$DOKKATOO_VERSION" + |} + | + """.trimMargin() + + createKotlinFile( + "src/main/kotlin/Hello.kt", + """ + |package com.project.hello + | + |/** The Hello class */ + |class Hello { + | /** prints `Hello` to the console */ + | fun sayHello() = println("Hello") + |} + | + """.trimMargin() + ) + + createKotlinFile("src/main/kotlin/HelloAgain.kt", "") + } + + dir("subproject-goodbye") { + + buildGradleKts = """ + |plugins { + | kotlin("jvm") version "1.8.22" + | id("org.jetbrains.dokka.dokkatoo") version "$DOKKATOO_VERSION" + |} + | + """.trimMargin() + + createKotlinFile( + "src/main/kotlin/Goodbye.kt", + """ + |package com.project.goodbye + | + |/** The Goodbye class */ + |class Goodbye { + | /** prints a goodbye message to the console */ + | fun sayHello() = println("Goodbye!") + |} + | + """.trimMargin() + ) + } + + config() + } +} |