Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ktlint 0.48.1 doesn't use experimental rules if .editorconfig is empty #1768

Closed
mateuszkwiecinski opened this issue Jan 8, 2023 · 6 comments
Milestone

Comments

@mateuszkwiecinski
Copy link
Contributor

mateuszkwiecinski commented Jan 8, 2023

We're trying to upgrade kotlinter to use the latest version and our tests caught the plugin doesn't respect experimental rules.

Here's the smallest repro I was able to come up with:

    @Test
    fun `ktlint 0-48-1`() {
        val standard = StandardRuleSetProvider()
        val experimental = ExperimentalRuleSetProvider()
        val ruleProviders = listOf(standard, experimental).flatMap { it.getRuleProviders() }.toSet()

        val ktLintEngine = KtLintRuleEngine(
            ruleProviders = ruleProviders,
            editorConfigOverride = EditorConfigOverride.EMPTY_EDITOR_CONFIG_OVERRIDE,
        )
        val errors = mutableListOf<LintError>()
        ktLintEngine.lint(
            code = Code.CodeSnippet(
                """
                val variable = "should not contain'()'".count() { it == 'x' }
    
                """.trimIndent(),
            ),
            callback = errors::add,
        )

        val failedRules = errors.map { it.ruleId }
        check(failedRules.contains("experimental:unnecessary-parentheses-before-trailing-lambda"))
        check(!failedRules.contains("filename"))
    }

Expected Behavior

  1. ktlint should respect experimental rules
  2. filename rule shouldn't fail for Code.CodeSnippet

Observed Behavior

If the editorconfig is empty, the test fails because ktlint doesn't run experimental checks against the code snippet. If the editorconfig contains any overrides the test passes. It seems like the VisitorProvider#isEnabled doesn't work well if the consumer has empty editorconfig.

Also, please note the check(!failedRules.contains("filename")) check, which I believe is a separate issue, the code currently emits LintError(line=1, col=1, ruleId=filename, detail=File name 'file.kt' should conform PascalCase) which I believe comes from here. Linting a Code.CodeSnipped shouldn't probably result in an filename error.

Steps to Reproduce

☝️
In the repro, while debugging the code here, the repro fails only when ruleExecutionContext.editorConfigProperties is empty here. Otherwise, experimental rulesets work as expected

Your Environment

  • Version of ktlint used: 0.48.1
  • Relevant parts of the .editorconfig settings - it has to be empty
  • Name and version (or code for custom task) of integration used (Gradle plugin, Maven plugin, command line, custom Gradle task): the repro uses raw kotlin dependency
  • Version of Gradle used (if applicable): all
  • Operating System and version: all
@lwasyl
Copy link

lwasyl commented Jan 8, 2023

I'll join with a related question: how should API consumers configure the rules? The API looks like passing ruleProviders will run all the rules provided, unless some of them were explicitly disabled in the .editorconfig somewhere. But that's not happening and as far as I see, the engine will:

  • run all the custom rules unless they're explicitly disabled
  • run all the standard rules unless they're explicitly disabled
  • not run any of the experimental rules unless they're explicitly enabled

This inconsistency seems confusing from the API usage perspective, IMO. Perhaps it'd make sense to keep the API straightforward, and make it responsibility of the wrappers (like CLI or Kotlinter) to properly filter out experimental rules based on the tool configuration?

@paul-dingemans
Copy link
Collaborator

I will come back on this issue next week earliest. I am having holidays and do not have to my laptop.

paul-dingemans added a commit to paul-dingemans/ktlint that referenced this issue Jan 15, 2023
@paul-dingemans
Copy link
Collaborator

If the editorconfig is empty, the test fails because ktlint doesn't run experimental checks against the code snippet. If the editorconfig contains any overrides the test passes. It seems like the VisitorProvider#isEnabled doesn't work well if the consumer has empty editorconfig.

This indeed is not correct. It will be fixed with #1772. It is not strictly related to the editorconfig being empty but whether property ktlint_experimental is not set.

Also, please note the check(!failedRules.contains("filename")) check, which I believe is a separate issue, the code currently emits LintError(line=1, col=1, ruleId=filename, detail=File name 'file.kt' should conform PascalCase) which I believe comes from here. Linting a Code.CodeSnipped shouldn't probably result in a filename error.

Indeed, the file name used for code snippets should not cause a violation by the filename rule. Will be fixed with #1778.

ktlint should respect experimental rules

This is the most difficult part of the issue to answer. I do understand that both of you advocate that all rules provided by the API Consumer have to be respected (e.g. to be run automatically). However, from the viewpoint of Ktlint, the end user (e.g. the developer who uses ktlint, kotlinter, or any other KtLint API consumer) should be in control on enabling or disabling experimental rules. This can best be done by providing the EditorConfigDefault value as shown below:

    @Test
    fun `ktlint 0-48-1`() {
        val standard = StandardRuleSetProvider()
        val experimental = ExperimentalRuleSetProvider()
        val ruleProviders = listOf(standard, experimental).flatMap { it.getRuleProviders() }.toSet()

        val ktLintEngine = KtLintRuleEngine(
            ruleProviders = ruleProviders,
            editorConfigDefaults = EditorConfigDefaults(
                EditorConfig
                    .builder()
                    .section(
                        Section
                            .builder()
                            .glob(Glob("*.kt"))
                            .properties(
                                Property
                                    .builder()
                                    .name("ktlint_experimental")
                                    .value("enabled"),
                            ),
                    )
                    .build()
            )
        )
        val errors = mutableListOf<LintError>()
        ktLintEngine.lint(
            code = Code.CodeSnippet(
                """
                val variable = "should not contain'()'".count() { it == 'x' }

                """.trimIndent(),
            ),
            callback = errors::add,
        )

        val failedRules = errors.map { it.ruleId }
        check(failedRules.contains("experimental:unnecessary-parentheses-before-trailing-lambda"))
        check(!failedRules.contains("filename"))
    }

If the user has set property ktlint_experimental explicitly than that value will be used. If the value is not defined, the value provided via editorConfigDefaults will be used.

If you do want to ignore the value of ktlint_experimental as set by the end user, than you can set the EditorConfigOverride property. But as said before that is discouraged as the end user will not understand why the .editorconfig property is being ignored (provided that the value set is not equal to the value provided by the API Consumer).

This inconsistency seems confusing from the API usage perspective, IMO. Perhaps it'd make sense to keep the API straightforward, and make it responsibility of the wrappers (like CLI or Kotlinter) to properly filter out experimental rules based on the tool configuration?

I have been thinking about this as well but disregarded it in the end. Some projects use multiple .editorconfig files for different parts of the project directory hierarchy or use different setting based on globs in a single .editorconfig file. The API Consumer has to provide all rules that possibly can be executed. Ideally, the end user should have the final say about what is executed.

Above explanation has also been added to the changelog via #1778 for other API Consumers.

@paul-dingemans paul-dingemans added this to the 0.48.2 milestone Jan 15, 2023
@paul-dingemans
Copy link
Collaborator

@mateuszkwiecinski @lwasyl Can you please try the latest snapshot of ktlint (https://pinterest.github.io/ktlint/install/snapshot-build/) to check if this helps?

@mateuszkwiecinski
Copy link
Contributor Author

mateuszkwiecinski commented Jan 18, 2023

@paul-dingemans I tested latest snapshot locally, and it seems to behave in an expected way 👍
Also, thanks for the clarification as it helps to understand the context. Especially the last part:

If you do want to ignore the value of ktlint_experimental as set by the user, than you can set the EditorConfigOverride property. But as said before that is discouraged as the user might not understand why the .editorconfig property is being ignored

🙏
I guess we (people involved in the PR) will have a discussion, and either do as you suggest, or since ktlint has now that extensive configuration via .editorconfig we probably could consider dropping additional config 👀

@paul-dingemans
Copy link
Collaborator

Tnx for confirmation. I will do some more tests and prepare for the 0.48.2 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants