Skip to content

polyvariant/sbt-scala-dotfiles

Repository files navigation

sbt-scala-dotfiles

An "sbt-github-actions"-like way to configure your Scala tooling dotfiles: declare your config as typed sbt keys and have the build generate the checked-in file. Four plugins live here, layered from specific to fully general — install whichever fits:

  • sbt-scala-dotfiles-scalafix — generate .scalafix.conf for Scalafix, including a separate file per configuration so Test can run a more relaxed rule set than Compile.
  • sbt-scala-dotfiles-scalafmt — generate .scalafmt.conf for scalafmt.
  • sbt-scala-dotfiles-hocon — generate any HOCON file(s) from typed value trees. The two tool plugins above are specializations of this one.
  • sbt-scala-dotfiles-files — the most general: maintain any files at all, as target -> exact content. The engine the others are built on.

They all share a small HOCON-rendering core and the same generate/check engine. The two tool plugins additionally wire the generated file into the upstream plugin (sbt-scalafix / sbt-scalafmt), so the IDE and the CLI still see a real checked-in config.

Status: playground. This is an experiment that sits on top of sbt-scalafix / sbt-scalafmt and drives them through their public keys.

Table of contents

Scalafix: sbt-scala-dotfiles-scalafix

Why

Scalafix is normally configured through a hand-written, checked-in .scalafix.conf (HOCON). That's a separate file to learn, with no connection to the rest of your build. This plugin lets you express the same configuration as typed sbt keys and generate the file, so:

  • there's one place to configure things — your build.sbt;
  • you can have different rules per configuration (the main motivation: a stricter Compile, a relaxed Test), which .scalafix.conf alone can't express;
  • the generated file is checked in, so the IDE and the Scalafix CLI still see a real .scalafix.conf.

Installation

In project/plugins.sbt (add sbt-scalafix too — this plugin builds on it):

addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.6")
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-scalafix" % version)

This plugin builds only for sbt 1.x / Scala 2.12, because sbt-scalafix does not publish an sbt 2.0 artifact.

Usage

Enable the plugin explicitly (it is not auto-applied) and declare your rules:

lazy val myProject = project
  .enablePlugins(ScalafixConfigPlugin)
  .settings(
    Compile / scalafixConfiguredRules := Seq("DisableSyntax", "OrganizeImports"),
    Compile / scalafixConfiguredSettings := Map(
      "DisableSyntax" -> Map("noFinalize" -> true),
      "OrganizeImports" -> Map(
        "targetDialect" -> "Scala3",
        "groups" -> Seq("re:javax?\\.", "scala", "*"),
      ),
    ),
  )

scalafixConfiguredGenerate (or scalafixConfiguredGenerateAll for every configuration) turns that into a .scalafix.conf next to your build:

# This file was automatically generated by the sbt-scala-dotfiles-scalafix plugin.
# Do not edit by hand — change your build's scalafixConfigured* keys instead
# and re-run scalafixConfiguredGenerate.

rules=[
    DisableSyntax,
    OrganizeImports
]
DisableSyntax {
    noFinalize=true
}
OrganizeImports {
    groups=[
        "re:javax?\\.",
        scala,
        "*"
    ]
    targetDialect=Scala3
}

Then run Scalafix as usual — sbt-scalafix picks up the generated file automatically:

sbt scalafixConfiguredGenerateAll   # writes .scalafix.conf and .scalafix.test.conf
sbt 'scalafix --check'
sbt 'Test / scalafix --check'

scalafixConfiguredSettings values may be any HOCON-representable shape: String, Boolean, numbers, null, Seq[Any] (lists), and nested Map[String, Any] (objects) — e.g. Map("Rule" -> Map("nested" -> Seq(1, 2, 3))) renders to a Rule { nested = [1, 2, 3] } block. They are rendered to HOCON by the Typesafe config library.

Relaxing the Test config

The scalafixConfigured* keys are independent per configuration — Test does not automatically inherit Compile. Since they're plain sbt settings, the idiomatic way to say "Test is Compile, minus a rule" is to derive one from the other. The common case — disabling a rule in Test while keeping everyone else's settings — is one line:

// Test = Compile, but drop OrganizeImports (its settings block comes along for the ride
// via the inherited scalafixConfiguredSettings, then we strip the now-unused key).
Test / scalafixConfiguredRules :=
  (Compile / scalafixConfiguredRules).value.filterNot(_ == "OrganizeImports"),
Test / scalafixConfiguredSettings :=
  (Compile / scalafixConfiguredSettings).value - "OrganizeImports",

This generates a separate .scalafix.test.conf, and Test / scalafix reads it (while Compile / scalafix keeps reading .scalafix.conf). Of course you can also just write the Test rules out from scratch if the two configs have little in common.

Keys

All scalafixConfigured* keys are scoped per configuration (Compile, Test). The *All tasks fan out over both.

Key Scope Description
scalafixConfiguredRules Compile, Test The rules to run, rendered as the rules = [...] array.
scalafixConfiguredSettings Compile, Test Per-rule settings, rendered as HOCON blocks.
scalafixConfiguredFile Compile, Test Output path. Defaults to .scalafix.conf (Compile) / .scalafix.test.conf.
scalafixConfiguredGenerate Compile, Test Render and write the .scalafix.conf for this configuration.
scalafixConfiguredCheck Compile, Test Fail if the on-disk file is out of date with the keys.
scalafixConfiguredGenerateAll project Run scalafixConfiguredGenerate for both Compile and Test.
scalafixConfiguredCheckAll project Run scalafixConfiguredCheck for both Compile and Test.

How it works

Scalafix has no API to accept configuration content in memory — ScalafixArguments.withConfig only takes a file path. So this plugin renders your keys to a real .scalafix.conf on disk (with a banner header pointing back here) and wires sbt-scalafix's own scalafixConfig setting to it, per configuration:

Compile / scalafixConfig := Some((Compile / scalafixConfiguredFile).value)  // when rules are set
Test    / scalafixConfig := Some((Test    / scalafixConfiguredFile).value)

Because sbt-scalafix reads scalafixConfig per configuration, Compile / scalafix and Test / scalafix each pick up their own generated file. The wiring only kicks in when a configuration actually declares rules; otherwise scalafixConfig is left untouched.

Keeping the generated file fresh in CI

The generated files are meant to be committed. To make sure they don't drift from the keys, run the check in CI:

sbt scalafixConfiguredCheckAll

It re-renders from the keys and fails (with a pointer to scalafixConfiguredGenerate) if the on-disk file differs.

scalafmt: sbt-scala-dotfiles-scalafmt

sbt-scala-dotfiles-scalafmt does the same thing for scalafmt: declare your scalafmt config as sbt keys and generate a .scalafmt.conf, wiring it into sbt-scalafmt.

It's independent of the Scalafix plugin — install whichever you need. Unlike Scalafix, scalafmt is configured by a single .scalafmt.conf (there's no Compile/Test split), so these keys are project-scoped rather than per-configuration.

In project/plugins.sbt (add sbt-scalafmt too — this plugin builds on it):

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6")
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-scalafmt" % version)

Enable the plugin explicitly (it is not auto-applied) and declare your config:

lazy val myProject = project
  .enablePlugins(ScalafmtConfigPlugin)
  .settings(
    scalafmtConfiguredVersion := "3.11.1",
    scalafmtConfiguredSettings := Map(
      "maxColumn" -> 100,
      "runner" -> Map("dialect" -> "scala213"),
      "rewrite" -> Map("rules" -> Seq("RedundantBraces", "RedundantParens")),
    ),
  )

scalafmtConfiguredGenerate turns that into a .scalafmt.conf next to your build:

# This file was automatically generated by the sbt-scala-dotfiles-scalafmt plugin.
# Do not edit by hand — change your build's scalafmtConfigured* keys instead
# and re-run scalafmtConfiguredGenerate.

version="3.11.1"
maxColumn=100
rewrite {
    rules=[
        RedundantBraces,
        RedundantParens
    ]
}
runner {
    dialect=scala213
}

scalafmt requires a version, so scalafmtConfiguredVersion is a typed key (everything else goes into the free-form scalafmtConfiguredSettings map). It defaults to the scalafmt version bundled with the sbt-scalafmt this plugin builds against (3.10.0); override it to pin your own.

Key Description
scalafmtConfiguredVersion The scalafmt version. Defaults to the bundled 3.10.0.
scalafmtConfiguredSettings Everything else, rendered as HOCON. Same value shapes as Scalafix's map.
scalafmtConfiguredFile Output path. Defaults to .scalafmt.conf.
scalafmtConfiguredGenerate Render and write the .scalafmt.conf.
scalafmtConfiguredCheck Fail if the on-disk file is out of date with the keys (run this in CI).

Enabling ScalafmtConfigPlugin means "I manage .scalafmt.conf from the build here": it always points sbt-scalafmt's scalafmtConfig at the generated file, so sbt scalafmtAll / scalafmtCheckAll and your editor pick it up.

Any HOCON file: sbt-scala-dotfiles-hocon

The scalafix and scalafmt plugins are specializations of a general HOCON generator. If you have other HOCON files to keep in sync with your build, use sbt-scala-dotfiles-hocon directly: declare target file -> value tree and the build renders each tree to HOCON and maintains it.

It has no upstream tool to wire into (and adds no banner — the rendered HOCON is the whole file), so unlike the tool plugins, you drive the generic managedFiles* tasks to generate and check.

In project/plugins.sbt:

addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-hocon" % version)
lazy val myProject = project
  .enablePlugins(HoconFilesPlugin)
  .settings(
    hoconFiles := Map(
      baseDirectory.value / "app.conf" -> Map(
        "maxColumn" -> 100,
        "runner" -> Map("dialect" -> "scala213"),
        "rules" -> Seq("A", "B"),
      )
    ),
  )
sbt managedFilesGenerate   # writes every file in hoconFiles (+ any raw managedFiles entries)
sbt managedFilesCheck      # fails if any is out of date (run this in CI)

hoconFiles value trees take the same shapes as the tool plugins' settings maps (String, Boolean, numbers, null, Seq[Any], nested Map[String, Any]). Entries are added to managedFiles, so you can mix HOCON files and raw files (see below) in one project.

Any file: sbt-scala-dotfiles-files

The most general plugin: maintain any files, as target -> exact content (written verbatim — no banner, no transformation). This is the engine every other plugin is built on; reach for it when the file isn't HOCON.

In project/plugins.sbt:

addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-files" % version)
lazy val myProject = project
  .enablePlugins(ManagedFilesPlugin)
  .settings(
    managedFiles := Map(
      baseDirectory.value / ".gitignore" -> "target/\n.bsp/\n",
      baseDirectory.value / "sub" / "VERSION" -> version.value,
    ),
  )
Key Description
managedFiles Files to maintain, as target path -> exact content (verbatim, no banner).
managedFilesGenerate Write every managed file.
managedFilesCheck Fail if any managed file is out of date — a missing file counts (run in CI).

Limitations

  • sbt 1.x / Scala 2.12 only (follows sbt-scalafix / sbt-scalafmt).
  • Custom-rule resolution is unchanged — declare custom rules with sbt-scalafix's scalafixDependencies as usual.
  • rules/version are always rendered first; the settings blocks that follow are emitted in sorted order (a property of the underlying config library), not source order. This is harmless — Scalafix and scalafmt parse the file either way — and keeps the output deterministic for the up-to-date check.
  • scalafmtConfiguredVersion can't be derived from sbt-scalafmt: scalafmt reads its version from .scalafmt.conf, and sbt-scalafmt exposes no key for it. The plugin's default tracks the version bundled with the sbt-scalafmt it builds against, so it can drift — pin your own.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages