Files
kotlin-fork/docs/analysis-api/development-process/contributing-guidelines.md
T

8.4 KiB

Contribution Guidelines

This document is a contribution guideline for the development of Analysis API project. Please, follow it for your changes to be accepted.

General Guidelines

  • Follow the Code Style
  • Write new code in Kotlin. It is totally okay to write Java code in existing Java files. You can always use Java To Kotlin Converter to convert it to Kotlin.
  • Always write Unit Tests for your changes.
  • Always perform code formatting and call import optimizer before committing your changes.
  • Do not use short names in declarations names
    • Bad: val dclSmbl: KtDeclarationSymbol
    • Good val declarationSymbol: KtDeclarationSymbol
  • Always add KDoc to any public declaration inside analysis-api module.
    • A good KDoc should include
      • What the declaration purpose and how it should be used?
      • Explanation of corner cases
      • Examples
  • Keep Analysis API Surface Area Concise and Minimal
  • Follow Analysis API Implementation Contracts
  • Keep your classes and functions with the lowest possible visibility
  • Properly design utility functions & do not overuse them

Guidelines in more details

Always add KDoc to any public declaration inside analysis-api module

This includes:

  • Public classes;
  • Public class members (functions/properties);
  • Class constructor parameters which are declared as public class members via val/var;
  • Top-level functions/properties.

Do not hesitate to make the KDoc as long and detailed as possible until the information you write will help others to use your API changes in a better way.

A good KDoc should include

What the declaration purpose and how it should be used?

Please do not include obvious KDocs, which do not add additional information. Always describe the purpose of the declaration.

Bad (adds no information to the declaration name):

public class KtAnnotationApplication(
    /**
     * ClassId of an annotation
     */
    val classId: ClassId
)

Good:

public class KtAnnotationApplication(
    /**
     * A fully qualified name of an annotation class which is being applied
     */
    val classId: ClassId
)

Explanation of corner cases and examples

Please, explain (better with examples) how the new functionality you add behaves on corner cases.

Example:

/**
 * Get type of given expression.
 *
 * Return:
 * - [KtExpression] type if given [KtExpression] is real expression;
 * - `null` for [KtExpression] inside packages and import declarations;
 * - `Unit` type for statements;
 */
public fun KtExpression.getKtType(): KtType?

Keep Analysis API Surface Area Concise and Minimal

As you already know, Analysis API is a compiler API used to retrieve compiler-related information for FIR IDE Plugin. It is used in a wide range of features: code completion, debugger, inspections, and all other features which require compiler-related information. So it is important to keep the Analysis API Surface Area concise. Whether you want to introduce a new method or change the behavior of the existing method, consider

  • Will your change be useful for others?
  • Will it be possible to implement your task without modifying Analysis API? If yes, maybe it would be a better solution to just implement the functionality you need as some utility function on your project side.

Always write Unit Tests for your code

It was already mentioned in the general part of the guidelines but Unit Tests are an important thing for public API. So, write Unit Test whenever you add new functionality or modify existing behavior (either fixing bug or adding feature). A good unit test:

  • Should cover basic usage scenarios
  • Should cover corner-cases

If you fixed a bug or added new functionality to an existing feature, consider adding test(s) which cover it.

Follow Naming Conventions

  • All public declarations which are exposed to the surface area of Analysis API should have Kt prefix. E.g, KtSymbol, KtConstantValue.

Follow Analysis API Implementation Contracts

  • Add ValidityTokenOwner as supertype for all declarations which contains other ValidityTokenOwner inside (eg, via parameter types, function return types) to ensure that internal ValidityTokenOwner are not exposed via your declaration.
  • You have some declaration which implements ValidityTokenOwner. It means that this declaration has a lifetime. And this declaration has to be checked to ensure that it is not used after its lifetime has come to the end. To ensure that all methods(except hashCode/equals /toString) and properties should be wrapped into withValidityAssertion { .. } check:
public class KtCall(
    private val _symbol: KtSymbol,
    private val _isInvokeCall: Boolean,
) : ValidityTokenOwner {
    public val symbol: KtSymbol get() = withValidityAssertion { _symbol }
    public val isInvokeCall: Boolean get() = withValidityAssertion { _isInvokeCall }

    public fun isImplicitCall(): Boolean = withValidityAssertion {
        // IMPL
    }

    override fun equals(other: Any?): Boolean { // no withValidityAssertion
        // IMPL
    }
    override fun hashCode(): Int { // no withValidityAssertion
        // IMPL
    }
    override fun toString(): String { // no withValidityAssertion
        // IMPL
    }
}

Keep your classes and functions with the lowest possible visibility

The only part of Analysis API which should be exposed is the Analysis API surface area (the API itself). All other declarations should be kept internal or private. To ensure that, analysis-api module has Library Mode enabled. This will enforce that only declarations which are supposed to be exposed are really exposed. There are no guarantees on the non-surface part of analysis API on binary and source compatibility.

Also, the implementation modules should be considered as internal themselves. Please, keep declarations there internal or private too then it is possible. Implementation modules are:

Properly design utility functions

In compiler-related code (and Analysis API is compiler-related 😀), there may be a lot of Kotlin top-level utility functions and properties to work with AST, PsiElements, and others. Such code pollutes public and internal namespaces and introduces a lot of functions with unclear semantics:

  • If you have a utility function that uses one or more private functions and those functions are used only by it, consider encapsulating the whole functions family into a class or object.

Bad:

internal fun render(value: KtAnnotationValue): String = buildString { renderConstantValue(value) }

private fun StringBuilder.renderConstantValue(value: KtAnnotationValue) {
    when (value) {
        is KtAnnotationApplicationValue -> renderAnnotationConstantValue(value)
            ...
    }
}

private fun StringBuilder.renderConstantAnnotationValue(value: KtConstantAnnotationValue) {
    append(value.constantValue.renderAsKotlinConstant())
}

// A lot of other non-related utility functions in the same file

Good:

internal object KtAnnotationRenderer {
    fun render(value: KtAnnotationValue): String = buildString { renderConstantValue(value) }

    private fun StringBuilder.renderConstantValue(value: KtAnnotationValue) {
        when (value) {
            is KtAnnotationApplicationValue -> renderAnnotationConstantValue(value)
                ...
        }
    }

    private fun StringBuilder.renderConstantAnnotationValue(value: KtConstantAnnotationValue) {
        append(value.constantValue.renderAsKotlinConstant())
    }
}
  • Your utility function may be useful for others or maybe very specific for your task. Please, decide if you want others to reuse your code or not. If not, and it is very task-specific, consider hiding it in your implementation. Otherwise, feel free to introduce it as a top-level function/object. For the latter case, it would be good to have a KDoc for it :)