diff --git a/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala b/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala index 306616c97..7c218d677 100644 --- a/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala +++ b/scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixArgumentsImpl.scala @@ -119,6 +119,8 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default) copy(args = args.copy(stdout = true)) case ScalafixMainMode.AUTO_SUPPRESS_LINTER_ERRORS => copy(args = args.copy(autoSuppressLinterErrors = true)) + case ScalafixMainMode.IN_PLACE_TRIGGERED => + copy(args = args.copy(triggered = true)) } override def withParsedArguments( diff --git a/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala b/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala index 8d7ab0d18..6088975ab 100644 --- a/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala +++ b/scalafix-cli/src/main/scala/scalafix/internal/v1/Args.scala @@ -84,6 +84,10 @@ case class Args( "configured in .scalafix.conf or via --rules" ) syntactic: Boolean = false, + @Description( + "Overlay the default rules & rule settings in .scalafix.conf with the `triggered` section" + ) + triggered: Boolean = false, @Description("Print out additional diagnostics while running scalafix.") verbose: Boolean = false, @Description("Print out this help message and exit") @@ -245,9 +249,22 @@ case class Args( RuleDecoder.decoder(ruleDecoderSettings.withConfig(scalafixConfig)) } + // With a --triggered flag, looking for settings in triggered block first, and fallback to standard settings. + def maybeOverlaidConfWithTriggered(base: Conf): Conf = + if (triggered) + ScalafixConfOps.overlay(base, "triggered") + else + base + def rulesConf(base: () => Conf): Conf = { if (rules.isEmpty) { - ConfGet.getKey(base(), "rules" :: "rule" :: Nil) match { + val rulesInConf = + ConfGet.getKey( + maybeOverlaidConfWithTriggered(base()), + "rules" :: "rule" :: Nil + ) + + rulesInConf match { case Some(c) => c case _ => Conf.Lst(Nil) } @@ -260,10 +277,13 @@ case class Args( base: Conf, scalafixConfig: ScalafixConfig ): Configured[Rules] = { + val targetConf = maybeOverlaidConfWithTriggered(base) + val rulesConf = this.rulesConf(() => base) val decoder = ruleDecoder(scalafixConfig) + val configuration = Configuration() - .withConf(base) + .withConf(targetConf) .withScalaVersion(scalaVersion) .withScalacOptions(scalacOptions) .withScalacClasspath(validatedClasspath.entries) diff --git a/scalafix-cli/src/main/scala/scalafix/internal/v1/ScalafixConfOps.scala b/scalafix-cli/src/main/scala/scalafix/internal/v1/ScalafixConfOps.scala new file mode 100644 index 000000000..a0c2f0197 --- /dev/null +++ b/scalafix-cli/src/main/scala/scalafix/internal/v1/ScalafixConfOps.scala @@ -0,0 +1,20 @@ +package scalafix.internal.v1 + +import metaconfig.Conf +import metaconfig.Conf.Obj +import metaconfig.ConfOps +import metaconfig.internal.ConfGet + +object ScalafixConfOps { + def drop(original: Conf, key: String): Conf = + ConfOps.fold(original)(obj = { confObj => + Obj(confObj.values.filterNot(_._1 == key)) + }) + + def overlay(original: Conf, key: String): Conf = { + val child = ConfGet.getKey(original, key :: Nil) + child.fold(original)( + ConfOps.merge(drop(original, key), _) + ) + } +} diff --git a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java index 53489fad2..99066cbfc 100644 --- a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java +++ b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixArguments.java @@ -106,7 +106,7 @@ ScalafixArguments withToolClasspath( ScalafixArguments withConfig(Optional config); /** - * @param mode The mode to run via --check or --stdout or --auto-suppress-linter-errors + * @param mode The mode to run via --check or --stdout or --auto-suppress-linter-errors or --triggered */ ScalafixArguments withMode(ScalafixMainMode mode); diff --git a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixMainMode.java b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixMainMode.java index b8af161a1..012c0da6f 100644 --- a/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixMainMode.java +++ b/scalafix-interfaces/src/main/java/scalafix/interfaces/ScalafixMainMode.java @@ -29,4 +29,8 @@ public enum ScalafixMainMode { */ AUTO_SUPPRESS_LINTER_ERRORS, + /** + * Use when the client triggers the run as a side effect of something else, as opposed to an explicit, interactive invocation. Write fixed contents in-place, with a custom configuration if it exists. + */ + IN_PLACE_TRIGGERED } diff --git a/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliSyntacticSuite.scala b/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliSyntacticSuite.scala index b5565c2fa..c17cc37f0 100644 --- a/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliSyntacticSuite.scala +++ b/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliSyntacticSuite.scala @@ -175,6 +175,34 @@ class CliSyntacticSuite extends BaseCliSuite { expectedExit = ExitStatus.Ok ) + check( + name = "--triggered is respected", + originalLayout = s"""| + |/.scalafix.conf + |rules = [ + | RemoveUnused + |] + | + |triggered.rules = [ProcedureSyntax] + | + |/hello.scala + |$original + |""".stripMargin, + args = Array("--triggered", "hello.scala"), + expectedLayout = s"""| + |/.scalafix.conf + |rules = [ + | RemoveUnused + |] + | + |triggered.rules = [ProcedureSyntax] + | + |/hello.scala + |$expected + |""".stripMargin, + expectedExit = ExitStatus.Ok + ) + check( name = "linter error", originalLayout = s"""/foobar.scala diff --git a/scalafix-tests/unit/src/test/scala/scalafix/tests/config/ArgsSuite.scala b/scalafix-tests/unit/src/test/scala/scalafix/tests/config/ArgsSuite.scala new file mode 100644 index 000000000..4c4c9c89c --- /dev/null +++ b/scalafix-tests/unit/src/test/scala/scalafix/tests/config/ArgsSuite.scala @@ -0,0 +1,75 @@ +package scalafix.tests.config + +import metaconfig.Conf +import metaconfig.internal.ConfGet +import scalafix.internal.v1.Args +import metaconfig.typesafeconfig.typesafeConfigMetaconfigParser +import scalafix.internal.config.ScalafixConfig + +class ArgsSuite extends munit.FunSuite { + + private lazy val givenConf = Conf + .parseString( + "ArgsSuite", + """ + |rules = [DisableSyntax, RemoveUnused] + | + |triggered.rules = [DisableSyntax] + | + |DisableSyntax.noVars = true + |DisableSyntax.noThrows = true + | + |triggered = { + | DisableSyntax.noVars = false + |} + | + |triggered.DisableSyntax.noReturns = true + |""".stripMargin + ) + .get + + test("ignore triggered section if args.triggered is false") { + val args = Args.default.copy(scalacOptions = "-Ywarn-unused" :: Nil) + val config = ScalafixConfig() + + assert(!args.triggered, "triggered should be false at default.") + + val rulesConfigured = args.configuredRules(givenConf, config).get + + assert( + rulesConfigured.rules + .map(_.name.value) == List("DisableSyntax", "RemoveUnused") + ) + + val merged = args.maybeOverlaidConfWithTriggered(givenConf) + + val disableSyntaxRule = ConfGet.getKey(merged, "DisableSyntax" :: Nil).get + + val expected = + Conf.Obj("noVars" -> Conf.Bool(true), "noThrows" -> Conf.Bool(true)) + + assertEquals(disableSyntaxRule, expected) + } + + test("use triggered section if args.triggered is true") { + val args = Args.default.copy(triggered = true) + val config = ScalafixConfig() + + val rulesConfigured = args.configuredRules(givenConf, config).get + + assert(rulesConfigured.rules.map(_.name.value) == List("DisableSyntax")) + + val merged = args.maybeOverlaidConfWithTriggered(givenConf) + + val disableSyntaxRule = ConfGet.getKey(merged, "DisableSyntax" :: Nil).get + + val expected = + Conf.Obj( + "noVars" -> Conf.Bool(false), + "noThrows" -> Conf.Bool(true), + "noReturns" -> Conf.Bool(true) + ) + + assertEquals(disableSyntaxRule, expected) + } +}