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

Add Spotless plugin support #4464

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

ayewo
Copy link
Contributor

@ayewo ayewo commented Feb 3, 2025

This PR adds official support in Mill for a SpotlessModule via the addition of 3 Spotless traits:

  • JavaSpotlessModule
  • KotlinSpotlessModule and
  • ScalaSpotlessModule

which can be used with JavaModule, KotlinModule and ScalaModule 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

Comment on lines 230 to 232
ktfmtVersion: String = "0.53",
ktfmtOptions: Option[KtfmtOptions] = None,
klintVersion: String = "1.5.0"
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@lihaoyi
Copy link
Member

lihaoyi commented Feb 8, 2025

I think the config looks fine as is. Let's drop the with updaters as mentioned earlier, then we can merge it

@lihaoyi
Copy link
Member

lihaoyi commented Feb 8, 2025

Is there a reason you want to use forwarders rather than using the .copy method directly?

@ayewo
Copy link
Contributor Author

ayewo commented Feb 8, 2025

No reason other than I thought you wanted it to be similar to the style of Spotless in Groovy.

@lihaoyi
Copy link
Member

lihaoyi commented Feb 8, 2025

Let's use the .copy methods directly then. We need to find a balance between the upstream style and the local Scala style, and in this case I think .copy wins out

@ayewo
Copy link
Contributor Author

ayewo commented Feb 8, 2025

I've updated to use .copy() directly now.

@ayewo ayewo requested review from lihaoyi and lefou February 11, 2025 12:40
@ayewo
Copy link
Contributor Author

ayewo commented Feb 12, 2025

@lihaoyi Anything else left for this to be merged ?

Comment on lines 15 to 35
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"))
}
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed as requested.

Copy link
Member

@lefou lefou left a 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.

@ayewo ayewo force-pushed the mill-spotless-plugin branch from 2a48a95 to e57ddfe Compare February 13, 2025 10:24
@ayewo
Copy link
Contributor Author

ayewo commented Feb 13, 2025

@lefou Done.

* Google Java Format version. Defaults to `1.25.2`.
*/
def googleJavaFormatVersion: T[String] = Task {
"1.25.2"
Copy link
Member

Choose a reason for hiding this comment

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

Where does this version come from? Is this the newest version? Do users expect it to stay stable or use always the newest?

We should not hardcode any versions. We either should leave it undefined or use a BuildInfo value that comes from the outer Mill build. That way, we can keep it up-to-date more easily.

* Google Java Format version. Defaults to `2.50.0`.
*/
def palantirJavaFormatVersion: T[String] = Task {
"2.50.0"
Copy link
Member

Choose a reason for hiding this comment

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

Same as for googleJavaFormatVersion, versions should not be hardcoded. Have a look at PR #4552, which replaces another instance of a hardcoded version.

* Defaults to `0.53`.
*/
def ktfmtVersion: T[String] = Task {
"0.53"
Copy link
Member

Choose a reason for hiding this comment

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

Should not be hardcoded. This version is already managed.

* Defaults to `1.5.0`.
*/
def ktlintVersion: T[String] = Task {
"1.5.0"
Copy link
Member

Choose a reason for hiding this comment

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

Use managed Versions.ktlintVersion instead.

* Scala Format version. Defaults to `3.8.1`.
*/
def scalafmtVersion: T[String] = Task {
"3.8.1"
Copy link
Member

Choose a reason for hiding this comment

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

Don't hardcode versions. This should be undefined or managed from the outer build.

sealed trait JavaFormatter

case class GoogleJavaFormat(
version: String = "1.25.2",
Copy link
Member

Choose a reason for hiding this comment

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

Don't hardcode versions.

}

case class PalantirJavaFormat(
version: String = "2.50.0",
Copy link
Member

Choose a reason for hiding this comment

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

Don't hardcode versions.

Comment on lines +187 to +190
ktfmtVersion: String = "0.53",
ktfmtOptions: Option[KtfmtOptions] = None,
ktlintFlag: Boolean = false,
ktlintVersion: String = "1.5.0"
Copy link
Member

@lefou lefou Feb 13, 2025

Choose a reason for hiding this comment

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

Don't hardcode versions. These are already available via the Versions object.

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

Successfully merging this pull request may close these issues.

Add Mill Spotless Plugin (500USD Bounty)
3 participants