All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pinterest.ktlint.rule.engine.internal.EditorConfigLoader.kt Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
package com.pinterest.ktlint.rule.engine.internal

import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults
import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults.Companion.EMPTY_EDITOR_CONFIG_DEFAULTS
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride.Companion.EMPTY_EDITOR_CONFIG_OVERRIDE
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.END_OF_LINE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES_EXECUTION_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue
import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAGS_ENABLED_PROPERTY
import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_OFF_ENABLED_PROPERTY
import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_ON_ENABLED_PROPERTY
import com.pinterest.ktlint.rule.engine.internal.ThreadSafeEditorConfigCache.Companion.THREAD_SAFE_EDITOR_CONFIG_CACHE
import mu.KotlinLogging
import org.ec4j.core.EditorConfigLoader
import org.ec4j.core.PropertyTypeRegistry
import org.ec4j.core.Resource
import org.ec4j.core.ResourcePropertiesService
import org.ec4j.core.model.Property
import org.ec4j.core.model.PropertyType
import org.ec4j.core.model.Version
import org.jetbrains.kotlin.utils.addToStdlib.applyIf
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path

private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()

/**
 * Loader for `.editorconfig` properties for files on [fileSystem].
 */
internal class EditorConfigLoader(
    private val fileSystem: FileSystem = FileSystems.getDefault(),
    private val editorConfigLoaderEc4j: EditorConfigLoaderEc4j,
    private val editorConfigDefaults: EditorConfigDefaults = EMPTY_EDITOR_CONFIG_DEFAULTS,
    private val editorConfigOverride: EditorConfigOverride = EMPTY_EDITOR_CONFIG_OVERRIDE,
) {
    /**
     * Loads properties used by [Rule]s from the `.editorconfig` file on given [filePath]. When [filePath] is null, the
     * properties for the ".kt" pattern in the current directory are loaded. The '.editorconfig' files on the [filePath]
     * are read starting from the [filePath] upwards until an '.editorconfig' file is found in which the property "root"
     * is found with value "true" or until the root of the filesystem is reached.
     *
     * Properties specified in [editorConfigDefaults] will be used in case the property was not found in any
     * '.editorconfig' on [filePath]. If the property is not specified in [editorConfigDefaults] then the default value
     * as specified in the property definition [EditorConfigProperty] is used.
     *
     * Properties specified in [editorConfigOverride] take precedence above any other '.editorconfig' file on [filePath]
     * or default value.
     */
    internal fun load(filePath: Path?): EditorConfig {
        val editorConfigPath = filePath ?: defaultFilePath()
        val editorConfigProperties: MutableMap =
            createResourcePropertiesService(editorConfigLoaderEc4j.editorConfigLoader, editorConfigDefaults)
                .queryProperties(editorConfigPath.resource())
                .properties
        return editorConfigProperties
            .also { properties ->
                if (properties[TAB_WIDTH_PROPERTY_NAME]?.sourceValue == properties[INDENT_SIZE_PROPERTY.name]?.sourceValue &&
                    editorConfigOverride.properties[INDENT_SIZE_PROPERTY] != null
                ) {
                    // The tab_width property can not be overridden via the editorConfigOverride. So if it has been
                    // set to the same value as the indent_size property then keep its value in sync with that
                    // property.
                    properties[TAB_WIDTH_PROPERTY_NAME] =
                        Property
                            .builder()
                            .name(TAB_WIDTH_PROPERTY_NAME)
                            .type(PropertyType.tab_width)
                            .value(editorConfigOverride.properties[INDENT_SIZE_PROPERTY]?.source)
                            .build()
                }
                editorConfigOverride
                    .properties
                    .forEach {
                        properties[it.key.name] = it.key.toPropertyWithValue(it.value)
                    }
            }.also { properties ->
                LOGGER.debug { properties.prettyPrint(filePath) }
            }.let { properties ->
                // Only add properties which are not related to rules but which are needed by the KtLint Rule Engine
                EditorConfig(properties)
                    .addPropertiesWithDefaultValueIfMissing(
                        /**
                         * Used in [EditorConfig] to determine the default value for a property when it is not specified in the
                         * [EditorConfig].
                         */
                        CODE_STYLE_PROPERTY,
                        /**
                         * Used by [KtLintRuleEngine] to use correct line separator when writing the formatted output.
                         */
                        END_OF_LINE_PROPERTY,
                        /**
                         * Used by [VisitorProvider] to determine whether experimental rules have to be executed.
                         */
                        EXPERIMENTAL_RULES_EXECUTION_PROPERTY,
                        /**
                         * Used by [FormatterTags] to determine whether formatter tags should be respected.
                         */
                        FORMATTER_TAGS_ENABLED_PROPERTY,
                        /**
                         * Used by [FormatterTags] to get the tag to disable the formatter.
                         */
                        FORMATTER_TAG_OFF_ENABLED_PROPERTY,
                        /**
                         * Used by [FormatterTags] to get the tag to enable the formatter.
                         */
                        FORMATTER_TAG_ON_ENABLED_PROPERTY,
                    )
            }
    }

    private fun MutableMap.prettyPrint(normalizedFilePath: Path?) =
        map { entry -> "${entry.key}: ${entry.value.sourceValue}" }
            .joinToString(
                prefix =
                    "Effective editorconfig properties${
                        if (normalizedFilePath == null) {
                            ""
                        } else {
                            " for file '$normalizedFilePath'"
                        }
                    }:\n\t",
                separator = "\n\t",
            )

    private fun Path?.resource() = Resource.Resources.ofPath(this, StandardCharsets.UTF_8)

    private fun defaultFilePath() =
        fileSystem
            .getPath(".")
            .toAbsolutePath()
            .resolve(SUPPORTED_FILES.first())

    private fun createResourcePropertiesService(
        editorConfigLoader: EditorConfigLoader,
        editorConfigDefaults: EditorConfigDefaults,
    ) = ResourcePropertiesService
        .builder()
        .keepUnset(true)
        .cache(THREAD_SAFE_EDITOR_CONFIG_CACHE)
        .loader(editorConfigLoader)
        .applyIf(editorConfigDefaults != EMPTY_EDITOR_CONFIG_DEFAULTS) {
            defaultEditorConfigs(editorConfigDefaults.value)
        }.build()

    companion object {
        /**
         * List of file extensions, editorconfig lookup will be performed.
         */
        internal val SUPPORTED_FILES =
            arrayOf(
                ".kt",
                ".kts",
            )

        private const val TAB_WIDTH_PROPERTY_NAME = "tab_width"
    }
}

/**
 * Wrapper around the native EditorConfigLoader of the ec4j library. Whenever Ktlints loads ".editorconfig" files via the ec4j library, it
 * needs to be aware of all native (ec4j) but also the custom property types defined in KtLint and the custom rules sets provided to KtLint.
 *
 * The ec4j library uses the convention that a property type has a name that is identical to the name of that property. If no
 * property type is registered for a property the property will be returned with a null type. When retrieving the property value it will be
 * returned as type "String" which results in class cast exceptions in KtLint's EditorConfig whenever the expected type of the variable is
 * not of type String.
 */
internal class EditorConfigLoaderEc4j(
    private val propertyTypes: Set>,
) {
    val editorConfigLoader: EditorConfigLoader
        get() = EditorConfigLoader.of(Version.CURRENT, propertyTypeRegistry(propertyTypes))

    private fun propertyTypeRegistry(propertyTypes: Set>) =
        PropertyTypeRegistry
            .builder()
            .defaults()
            .apply {
                propertyTypes.forEach { type(it) }
            }.build()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy