Skip to content

spacedvoid/constructor-like

Repository files navigation

ConstructorLike

Dokka plugin for documenting pseudo-constructors for Kotlin.

Pseudo-constructors?

Most object-oriented languages have constructors that can be used to create an instance of a class. But mostly, we need some preprocessing with the arguments: validating, transforming, etc. Java introduced flexible constructor bodies for this purpose, but Kotlin cannot have such feature because we're stuck at this C-style constructor syntax:

class MyClass(val i: Int) { 
	constructor(s: String): this(s.toInt()) // Might cause `IllegalArgumentException`
}

Or we might have an interface that can be instantiated by everyone, but giving a helper function would be great:

interface IntWrapper {
	val i: Int
}

fun wrap(i: Int): IntWrapper = object: IntWrapper {
	override val i: Int = i
}

And sometimes we need a generic constructor:

class Logger<T>(val forClass: KClass<T>)

inline fun <reified T> T.createLogger(): Logger = Logger(T::class)

This leads us to using functions that look like constructors:

  1. operator fun invoke functions in companion objects or an extension to one
  2. Functions with the same name of the class (fun Char())

These are what we call pseudo-constructors, since they don't look different with normal constructors: MyClass(<parameters>)

This plugin injects such functions to the Constructors table of a class documentation: example-doc

Because they are technically not constructors, this is purely decorative: navigating to such constructor leads to the actual definition of the pseudo-constructor, which at the example above is an operator fun invoke in a companion object, and the function's name is displayed at the first column of the Constructor table for when using it as a function reference(::MyClass).

Specification details

Summary: If the function can be used with the same syntax as normal constructors in the class and is annotated with @ConstructorLike, it is a pseudo-constructor.

To make a function as a pseudo-constructor, it must be annotated with @ConstructorLike. Other functions will not be included.

The target type of the annotated function is its return type. The target type must not be kotlin.Unit, kotlin.Nothing, an annotation class, enum class, or object. It must also be in the same module and package with the function so that the plugin can inject the constructor to the documentation.

Then, the function must be an operator fun invoke or its name must match the target type's simple name.

For ease of parsing, we define a receiver type of the function: for extension functions, it is the receiver itself, and for member functions, it is the classlike owning the function. While a function can have no receiver type in case it is a package-level non-extension non-operator fun invoke function, it cannot have two receiver types: the function cannot be an extension member function.

Finally, it is validated based on the receiver type's kind:

  • companion object: the target type must be the parent of the companion if the function is an operator fun invoke, otherwise the target type must be a nested class of the parent of the receiver type. Other functions that do not target the parent classlike will be parsed regarding the companion as an object.
  • Plain classlikes: the function must not be an operator fun invoke, the target type must be a nested class of the receiver type, and if the receiver type is not an object, the target type must also be inner.
  • No receiver type: the function must not be an operator fun invoke, and the target type must be a package-level classlike.

If the function violates anything from above, the plugin will raise a warning and will not include the function as a pseudo-constructor.

Examples

To be brief, function bodies are omitted.

class MyClass { // Also applies to abstract classes and interfaces
	class NestedClass

	inner class InnerClass

	companion object {
		class NestedInObject // Also applies to nested classes in standalone objects
		
		// OK: used as `MyClass()`
		@ConstructorLike
		operator fun invoke(): MyClass

		// Bad: it is not marked `operator`
		@ConstructorLike
		fun invoke(): MyClass
		// Will not be presented afterward, but applies on all cases.

		// Bad: it is an extension
		@ConstructorLike
		operator fun Any.invoke(): MyClass

		// Bad: it does not return `MyClass`
		@ConstructorLike
		operator fun invoke(): Any
		// In practice, this will cause a 'target in different module' warning. 

		// OK: used as `MyClass.NestedClass()`
		@ConstructorLike
		fun NestedClass(): NestedClass

		// Bad: it is an extension
		@ConstructorLike
		fun Any.NestedClass(): NestedClass
		// Will not be presented afterward, the function can either be an extension or a member, but not both.

		// Bad: it is not named `NestedClass`
		@ConstructorLike
		fun createNested(): NestedClass
		// Will not be presented afterward, but if the function is not an `operator fun invoke`, its name must match its target type.

		// Bad: it does not return `NestedClass`
		@ConstructorLike
		fun NestedClass(): Any
		// Will not be presented afterward, same reason as above.

		// Bad: target type is `InnerClass`
		@ConstructorLike
		fun InnerClass(): InnerClass
		// You cannot create inner classes with a companion object in any way.
		
		// OK: used as `MyClass.Companion.NestedInObject()`
		@ConstructorLike
        fun NestedInObject(): NestedInObject
	}

	// Bad: target type is `NestedClass`
	@ConstructorLike
	fun NestedClass(): NestedClass
	// You cannot create nested classes with the outer class in any way, except using companion objects.

	// OK: used as `myClassInstance.InnerClass()`
	@ConstructorLike
	fun InnerClass(): InnerClass

	// Bad: it does not return `InnerClass`
	@ConstructorLike
	fun InnerClass(): Any

	// Bad: it is an `operator fun invoke`
	@ConstructorLike
	operator fun invoke(): InnerClass
}

// OK: used as `MyClass()`
@ConstructorLike
operator fun MyClass.Companion.invoke(): MyClass

// Bad: it is not an extension function on `MyClass.Companion`
@ConstructorLike
operator fun MyClass.invoke(): MyClass

// Bad: it does not return `MyClass`
@ConstructorLike
operator fun MyClass.Companion.invoke(): Any

// OK: used as `MyClass()`
@ConstructorLike
fun MyClass(): MyClass

// Bad: it is an extension
@ConstructorLike
fun Any.MyClass(): MyClass

// OK: used as `MyClass.NestedClass()`
@ConstructorLike
fun MyClass.Companion.NestedClass(): NestedClass

// Bad: it is not an extension on `MyClass.Companion`
@ConstructorLike
fun MyClass.NestedClass(): NestedClass

// OK: used as `myClassInstance.InnerClass()`
@ConstructorLike
fun MyClass.InnerClass(): InnerClass

// Bad: it is not an extension on `MyClass`
@ConstructorLike
fun MyClass.Companion.InnerClass(): InnerClass

// OK: used as `MyClass.Companion.NestedInObject()`
@ConstructorLike
fun MyClass.Companion.NestedInObject(): NestedInObject

In case of expect/actual declarations, only the expect matters since actual declarations must match the signature of expect. But make sure to annotate actual declarations with @ConstructorLike too, otherwise the behavior of the plugin is undefined.

License

This project's source code is mainly licensed with MPL 2.0. Note that some files have different licenses other than MPL: currently, only DefaultPageCreatorFunctions.kt is licensed with Apache 2.0.

For more third-party license information, see third-party-licenses/.

About

Dokka plugin for documenting constructor-like functions

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Languages