Skip to content
New issue

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

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

Already on GitHub? # to your account

Migrate plugin to use KtLint directly instead invoking it via CLI. #424

Merged
merged 58 commits into from
Feb 5, 2021

Conversation

Tapchicoma
Copy link
Collaborator

@Tapchicoma Tapchicoma commented Jan 3, 2021

Main motivation of this huge refactoring is to migrate plugin to use directly KtLint class instead of doing javaexec for CLI.

Several reasons are behind this:

  • Gain more control over how to run ktlint and how to output errors
  • CLI creates thread pool itself to run checks in parallel. This thread pool competes over CPU resources with Gradle daemon threads, when plugin is used in multimodule project and org.gradle.parallel flag is enabled.
  • Use more optimized approach to preload info and run lint checks itself (more about it below)

This PR introduces new tasks and task hierarchy, plus uses workers everywhere. WorkActions are running in isolated classloader mode, allowing to still support seamless KtLint version changes without updating plugin itself (unless KtLint will have some breaking changes).

Following task structure is created:

  • LoadRuleSetsTask - preloads RuleSetProviders and serialize them into intermediate file. Such approach allows to skip this step on incremental run and start doing linting directly. - After some testing, I see this task adds no performance gain and I removed it.
  • LoadReportersTask - preloads ReporterProvider and based on enabled and custom reporters serialize two files - one with only enabled ReporterProviders, second with some useful info for GenerateReportsTask. Again such approach allows to skip this step on incremental run and start doing linting directly.
  • BaseKtlintCheckTask* runs as previously actual linting. Currently only one WorkAction is submitted for all files in SourceSet.
  • GenerateReportsTask - generates actual reports and writes errors into Gradle console. This task will fail the build if errors were not autocorrected and present. Each report generation is running in own WorkAction, plus console output is additional WorkAction.

Added a tests to verify supported KtLint versions are working correctly and they are besides two:

  • 0.37.0 on Windows crashes. This was fixed in 0.37.1 patch release.
  • 0.38.0 on all OS crashes, because it was build with Kotlin api 1.4 level that is not supported yet by Gradle. Issue was fixed in 0.38.1 patch release.

Did some testing on average project (312 modules, 4150 Kotlin files, 240_000 lines of code):

  • current ktlint-gradle release has around 7 min runtime to check all
  • snapshot release from this PR has around 2.5 min runtime to check all

Single SourceSet check runtime is slightly increased now. For example, for module with 232 Kotlin files it is increased from 10.1s to 17.4s. This single module performance issue could be addressed by splitting files into buckets in followup PR.

Changes itself fix #395 and #229 issues.

This file has new package suffix - "worker".
So later it could be restored from file and consumed on running KtLint.
Now it directly runs KtLint rather then invoking it via CLI.
This laoded reporters are serialized into file for later consume.
And wire them up to use LoadReportersTask and LoadRuleSetsTasks.
Save loaded reporter providers into one file and save into another file
loaded reporter providers ids, file extensions and optional addtional
reporter options.
This task will also fail the whole build in case of any non-corrected
lint issue.
Task themselves and in tests.
disabledRules: Set<String>
): Set<RuleSet> = loadRuleSetsFromClasspath()
.filterKeys { enableExperimental || it != "experimental" }
.filterKeys { !(disabledRules.contains("standard") && it == "\u0000standard") }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we get a string starting with the null character here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is copy-paste from KtLint CLI: https://github.com/pinterest/ktlint/blob/aa03a6b6b74cf71d8c2ede0c0a5e2305ce2e4327/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/RuleSetsLoader.kt#L24

Initially it was introduced by Shyiko to enforce "standard" ruleset to be always the first.

.associateBy {
val key = it.get().id
// Adapted from KtLint CLI module
if (key == "standard") "\u0000$key" else key
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the null character present here?

Copy link
Collaborator Author

@Tapchicoma Tapchicoma Jan 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in the previous comment

Copy link
Owner

@JLLeitschuh JLLeitschuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think two things:

  1. Add archunt tests to verify that ktlint classes aren't called where they shouldn't be.
  2. Do the finalizeBy thing suggested by Paul.

@pbielicki
Copy link

I see that it's moving which is a good news. @Tapchicoma any idea when it can be merged / released?

@Tapchicoma
Copy link
Collaborator Author

@pbielicki haven't had time before, but has a plan to address review comments till Friday.

…guration.

This should reduce probability of strange errors, because 3rd party
ruleset or reporter are using newer version of shared with KtLint
dependencies.
@booniepepper
Copy link

If time is the bottleneck for these last bits, let me know. I could carve out some time next week

@Tapchicoma
Copy link
Collaborator Author

@JLLeitschuh

Add archunt tests to verify that ktlint classes aren't called where they shouldn't be.

Please check c899d63 commit.

Do the finalizeBy thing suggested by Paul.

From my tests if worker in linting task fails the build, finalizer task is not called (from documentation I would expected otherwise). I've asked related question in Gradle slack https://gradle-community.slack.com/archives/CA745PZHN/p1611928971081900, but nobody answered if it is expected behaviour 🤷‍♂️

TLDR: I would propose to leave as it is currently.

Copy link
Owner

@JLLeitschuh JLLeitschuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just one final question and we can merge this.

Comment on lines +83 to +132
private fun Configuration.ensureConsistencyWith(
target: Project,
otherConfiguration: Configuration
) {
if (GradleVersion.version(target.gradle.gradleVersion) >= GradleVersion.version("6.8")) {
shouldResolveConsistentlyWith(otherConfiguration)
} else {
// Inspired by
// https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/ConstraintHandler.kt
incoming.beforeResolve {
val configName = it.name
otherConfiguration.incoming.resolutionResult.allDependencies { dependency ->
if (dependency is ResolvedDependencyResult) {
val id = dependency.selected.id
if (id is ModuleComponentIdentifier) {
// using a repository with a flatDir to stock local AARs will result in an
// external module dependency with no version.
if (!id.version.isNullOrEmpty()) {
if (id.module != "listenablefuture" ||
id.group != "com.google.guava" ||
id.version != "1.0"
) {
target.dependencies.constraints.add(
configName,
"${id.group}:${id.module}:${id.version}"
) { constraint ->
constraint.because("${otherConfiguration.name} uses version ${id.version}")
constraint.version { versionConstraint ->
versionConstraint.strictly(id.version)
}
}
}
}
} else if (id is ProjectComponentIdentifier &&
id.build.isCurrentBuild &&
dependency.requested is ModuleComponentSelector
) {
// Requested external library has been replaced with the project dependency, so
// add the project dependency to the target configuration, so it can be chosen
// instead of the external library as well.
// We should avoid doing this for composite builds, so we check if the selected
// project is from the current build.
target.dependencies.add(
configName,
target.dependencies.project(mapOf("path" to id.projectPath))
)
}
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have some kind of test verifying that this does what we expect it to do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, check this test

By running on lowest supported and current one Gradle versions - both branches are been verified.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

Copy link
Owner

@JLLeitschuh JLLeitschuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! This was a boatload of work. Thanks for doing this!

@Tapchicoma
Copy link
Collaborator Author

For those who is interested - I will try to push new release tomorrow.

@Tapchicoma Tapchicoma merged commit afa1b0d into JLLeitschuh:master Feb 5, 2021
@Tapchicoma Tapchicoma deleted the use-ktlint-core branch February 5, 2021 13:40
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
5 participants