-
-
Notifications
You must be signed in to change notification settings - Fork 390
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
Add Spotless plugin support #4464
base: main
Are you sure you want to change the base?
Conversation
ktfmtVersion: String = "0.53", | ||
ktfmtOptions: Option[KtfmtOptions] = None, | ||
klintVersion: String = "1.5.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some way to configure whether Spotless uses ktfmt
or ktlint
? AFAIK both of them provide formatters. Or is Spotless hardcoded to use one or the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's is currently no way to configure whether Spotless uses ktlint
. It's currently set as the default.
If a valid config for ktfmt
is provided via KotlinConfig(...)
in the build.mill
, then ktfmt
will also be used, alongside the default formatter i.e. ktlint
.
You want both of them to be configurable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need then, I think we can just follow the upstream convention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on your answer, it sounds like you were asking about upstream Spotless, not my implementation?
My answer was regarding my implementation.
If you were specifically asking regarding the Spotless project, this is how they do it for Kotlin:
kotlin {
// by default the target is every '.kt' and '.kts` file in the java sourcesets
ktfmt() // has its own section below
ktlint() // has its own section below
diktat() // has its own section below
prettier() // has its own section below
licenseHeader '/* (C)$YEAR */' // or licenseHeaderFile
}
In the example build.gradle
file above, the presence of ktfmt()
would correspond to the invocation of the default version of ktfmt
via this config method ktfmt()
.
kotlin {
ktfmt("0.51")
...
}
In the example above, passing a specific version i.e. ktfmt("0.51")
would invoke a similar config method that takes a version argument: ktfmt(String version)
.
kotlin {
licenseHeader '/* (C)$YEAR */' // or licenseHeaderFile
}
In this example, ktfmt
will be skipped entirely since it is completely absent from the steps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the upstream convention would mean refactoring KotlinConfig
and similar case classes to look like this:
case class KotlinConfig(
target: String = ".kt",
licenseHeader: Option[String] = None,
licenseHeaderFile: Option[String] = None,
override val licenseHeaderDelimiter: String = "(package |@file|import )",
ktfmt: Boolean = false,
ktfmtVersion: String = "0.53",
ktfmtOptions: Option[KtfmtOptions] = None,
klint: Boolean = false,
klintVersion: String = "1.5.0"
) extends JVMLangConfig {
require(
!(licenseHeader.isDefined && licenseHeaderFile.isDefined),
"Please specify only licenseHeader or licenseHeaderFile but not both"
)
def ktfmt(): KotlinConfig =
copy(ktfmt = true)
def ktfmt(version: String): KotlinConfig =
copy(ktfmt = true, ktfmtVersion = version)
def ktfmtOptions(options: KtfmtOptions): KotlinConfig =
copy(ktfmtOptions = Some(options))
def ktlint(): KotlinConfig =
copy(klint = true)
def ktlint(version: String): KotlinConfig =
copy(klint = true, klintVersion = version)
...
}
With this change, ktlint
would no longer added as a formatter step by default.
I think the config looks fine as is. Let's drop the |
Is there a reason you want to use forwarders rather than using the |
No reason other than I thought you wanted it to be similar to the style of Spotless in Groovy. |
Let's use the |
I've updated to use |
@lihaoyi Anything else left for this to be merged ? |
val googleFormatter = GoogleJavaFormat() | ||
.copy(version = "1.25.2") | ||
.copy(aosp = true) | ||
.copy(reflowLongStrings = true) | ||
.copy(formatJavadoc = false) | ||
.copy(reorderImports = false) | ||
.copy(groupArtifact = "com.google.googlejavaformat:google-java-format") | ||
|
||
val palantirFormatter = PalantirJavaFormat() | ||
.copy(version = "2.50.0") | ||
.copy(style = "GOOGLE") | ||
.copy(formatJavadoc = true) | ||
|
||
def jvmLangConfig = new JavaConfig() | ||
.copy(importOrder = Some(Seq())) | ||
.copy(formatter = Some(googleFormatter)) | ||
.copy(formatter = | ||
Some(palantirFormatter) | ||
) // this replaces `googleFormatter` as only 1 `JavaFormatter` can be active | ||
.copy(licenseHeader = Some("/* (C) 2025. Licensed under Apache-2.0. */\n")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not need to use .copy
here at all I think, we can just use the GoogleJavaFormat(...)
constructor. Same as the others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed as requested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to have the top-level classes in their own files.
2a48a95
to
e57ddfe
Compare
@lefou Done. |
I believe I've fixed all instances of versions being hard-coded. This is ready to be merged @lihaoyi. |
@ayewo the example test seems to be failing, you can find instructions to run it locally in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might have slipped thru in previous reviews, but it looks like you expecting the spotless library on the build classpath, whereas what we want is the SpotlessModule
to load all necessary libraries dynamically on demand. The whole point of abstracting over the spotless API is, that spotless itself is not on the classpath and will only be loaded dynamically based on the version configured in the module. All code that is coded against any spotless API needs therefore to reside in a dedicated module.
For example, ZincWorkerModule
is resposible to abstract over a concrete zinc dependency. The shared API (ZinkWorkerApi
) is loaded from the mill.scalalib.api
module, whereass the implemntation that has access to the actual zinc API resides in mill.scalalib.worker
and is loaded on demand by ZincWorkerModule
.
@@ -0,0 +1,3 @@ | |||
# Newer versions won't work with Java 8! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this comment is correct
I don't understand what you are saying here. The main module doing all the heavy-lifting is trait SpotlessModule extends CoursierModule |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me point out some issues or indicators, that we load or are required to load the spotless API on the Mill classpath. Beside the fact that SpotlessModule
extends
CoursierModule
, all com.diffplug.spotless._
imports don't get loaded via that CoursierModule
but are rather already on the classpath, as you added spotless to the dependencies of mill.scalalib
module.
@@ -12,6 +12,7 @@ object `package` extends MillBuildRootModule { | |||
ivy"com.goyeau::mill-scalafix::0.5.0", | |||
ivy"com.lihaoyi::mill-main-graphviz:${mill.api.BuildInfo.millVersion}", | |||
// TODO: document, why we have this dependency | |||
ivy"org.jsoup:jsoup:1.18.1" | |||
ivy"org.jsoup:jsoup:1.18.1", | |||
ivy"com.diffplug.spotless:spotless-lib:3.0.2" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not required to add spotless-lib
to the Mill classpath.
import java.nio.file.Paths | ||
import java.io.File | ||
import java.io.IOException | ||
import com.diffplug.spotless.{Provisioner, Formatter, LineEnding} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import means, the spotless API is already on the Mill classpath.
import com.diffplug.spotless.FormatterStep | ||
import com.diffplug.spotless.kotlin.{KtfmtStep, KtLintStep} | ||
import com.diffplug.spotless.kotlin.KtfmtStep.KtfmtFormattingOptions | ||
import com.diffplug.spotless.scala.ScalaFmtStep | ||
import com.diffplug.spotless.java.{ | ||
GoogleJavaFormatStep, | ||
PalantirJavaFormatStep, | ||
ImportOrderStep, | ||
RemoveUnusedImportsStep, | ||
CleanthatJavaStep, | ||
FormatAnnotationsStep | ||
} | ||
import com.diffplug.spotless.generic.LicenseHeaderStep | ||
import com.diffplug.spotless.Provisioner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These imports mean, we need spotless on the Mill classpath.
@@ -18,7 +18,7 @@ object `package` extends RootModule with build.MillStableScalaModule { | |||
|
|||
def moduleDeps = Seq(build.main, build.scalalib.api, build.testrunner) | |||
def ivyDeps = { | |||
Agg(build.Deps.scalafmtDynamic, build.Deps.scalaXml) ++ { | |||
Agg(build.Deps.scalafmtDynamic, build.Deps.scalaXml, build.Deps.spotless) ++ { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dependency forced spotless on the Mill classpath.
@lefou
Currently putting together the documentation changes that Li requested. I finished the core of this ticket more than 2 weeks ago. The only thing keeping this from being merged are nits that were pointed out during review. It's not nice at all, to point out nits, a contributor fixes them, the cycle repeats, then you come swinging pointing out a fresh set of nits. Please be respectful of contributors time. |
@ayewo I'm sorry if you feel that way. And I understand your emotions under the assumptions, these are nits. But those points are not nits. And it's not only your time that is spent here. Until now I only commented on smaller parts under the assumption that the main feature was already reviewed by @lihaoyi. Now, I recognized some major issues and felt the need to point them out. I understand, that the amount of work to finish this PR is likely higher compared to e.g. implementing Just to make it clear, my latest review points are not nits. Your PR description claims to do it the same way as other modules, yet all those other modules don't add any dependencies to I'm glad to help you with the module split-up, if you need help. My main focus is to get a new feature into Mill without breaking all other properties and users of Mill. |
@lefou's is correct, the issues he raised are important, I missed them in my own review, and we should resolve them before merging this. I apologize for not being thorough enough in my own review. @lefou has offered to help get it in if you would like it. Some friction in getting PRs in is expected. If we were all sitting in the same room this would be smoother, but we're scattered around the world, so we'll have to live with some inefficiency. |
This PR adds official support in Mill for a
SpotlessModule
via the addition of 3 Spotless traits:JavaSpotlessModule
KotlinSpotlessModule
andScalaSpotlessModule
which can be used with
JavaModule
,KotlinModule
andScalaModule
respectively. This is because each of those 3 JVM languages have different 3rd-party dependencies.It works similar in style to
PalantirFormatModule
.This is intended to close #3888