diff --git a/build.sbt b/build.sbt index 3720952a..7a74814c 100644 --- a/build.sbt +++ b/build.sbt @@ -151,6 +151,7 @@ lazy val scalafix = project name := "sbt-typelevel-scalafix", tlVersionIntroduced := Map("2.12" -> "0.4.10") ) + .dependsOn(noPublish) lazy val ciSigning = project .in(file("ci-signing")) diff --git a/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProject.scala b/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProject.scala new file mode 100644 index 00000000..d54cc0b7 --- /dev/null +++ b/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProject.scala @@ -0,0 +1,126 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.sbt + +import sbt._ +import scalafix.sbt._ + +import Keys._ +import ScalafixTestkitPlugin.autoImport._ + +final class ScalafixProject private ( + val name: String, + val rules: Project, + val input: Project, + val output: Project, + val tests: Project +) extends CompositeProject { + + lazy val componentProjects = Seq(rules, input, output, tests) + + def componentProjectReferences = componentProjects.map(x => x: ProjectReference) + + def in(dir: File): ScalafixProject = + new ScalafixProject( + name, + rules.in(dir / "rules"), + input.in(dir / "input"), + output.in(dir / "output"), + tests.in(dir / "tests") + ) + + def rulesSettings(ss: Def.SettingsDefinition*): ScalafixProject = + rulesConfigure(_.settings(ss: _*)) + + def inputSettings(ss: Def.SettingsDefinition*): ScalafixProject = + inputConfigure(_.settings(ss: _*)) + + def outputSettings(ss: Def.SettingsDefinition*): ScalafixProject = + outputConfigure(_.settings(ss: _*)) + + def testsSettings(ss: Def.SettingsDefinition*): ScalafixProject = + testsConfigure(_.settings(ss: _*)) + + def rulesConfigure(transforms: (Project => Project)*): ScalafixProject = + new ScalafixProject( + name, + rules.configure(transforms: _*), + input, + output, + tests + ) + + def inputConfigure(transforms: (Project => Project)*): ScalafixProject = + new ScalafixProject( + name, + rules, + input.configure(transforms: _*), + output, + tests + ) + + def outputConfigure(transforms: (Project => Project)*): ScalafixProject = + new ScalafixProject( + name, + rules, + input, + output.configure(transforms: _*), + tests + ) + + def testsConfigure(transforms: (Project => Project)*): ScalafixProject = + new ScalafixProject( + name, + rules, + input, + output, + tests.configure(transforms: _*) + ) + +} + +object ScalafixProject { + def apply(name: String): ScalafixProject = { + + lazy val rules = Project(s"$name-rules", file(s"$name/rules")).settings( + libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % _root_ + .scalafix + .sbt + .BuildInfo + .scalafixVersion + ) + + lazy val input = + Project(s"$name-input", file(s"$name/input")).enablePlugins(NoPublishPlugin) + + lazy val output = + Project(s"$name-output", file(s"$name/output")).enablePlugins(NoPublishPlugin) + + lazy val tests = Project(s"$name-tests", file(s"$name/tests")) + .settings( + scalafixTestkitOutputSourceDirectories := (output / Compile / unmanagedSourceDirectories).value, + scalafixTestkitInputSourceDirectories := (input / Compile / unmanagedSourceDirectories).value, + scalafixTestkitInputClasspath := (input / Compile / fullClasspath).value, + scalafixTestkitInputScalacOptions := (input / Compile / scalacOptions).value, + scalafixTestkitInputScalaVersion := (input / Compile / scalaVersion).value + ) + .dependsOn(rules) + .enablePlugins(NoPublishPlugin, ScalafixTestkitPlugin) + + new ScalafixProject(name, rules, input, output, tests) + } +} diff --git a/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProjectMacros.scala b/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProjectMacros.scala new file mode 100644 index 00000000..a60f6187 --- /dev/null +++ b/scalafix/src/main/scala/org/typelevel/sbt/ScalafixProjectMacros.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.sbt + +import scala.annotation.tailrec +import scala.reflect.macros.blackbox + +private[sbt] object ScalafixProjectMacros { + + // Copied from sbt.std.KeyMacro + def definingValName(c: blackbox.Context, invalidEnclosingTree: String => String): String = { + import c.universe.{Apply => ApplyTree, _} + val methodName = c.macroApplication.symbol.name + def processName(n: Name): String = + n.decodedName + .toString + .trim // trim is not strictly correct, but macros don't expose the API necessary + @tailrec def enclosingVal(trees: List[c.Tree]): String = { + trees match { + case ValDef(_, name, _, _) :: _ => processName(name) + case (_: ApplyTree | _: Select | _: TypeApply) :: xs => enclosingVal(xs) + // lazy val x: X = has this form for some reason (only when the explicit type is present, though) + case Block(_, _) :: DefDef(mods, name, _, _, _, _) :: _ if mods.hasFlag(Flag.LAZY) => + processName(name) + case _ => + c.error(c.enclosingPosition, invalidEnclosingTree(methodName.decodedName.toString)) + "" + } + } + enclosingVal(enclosingTrees(c).toList) + } + + // Copied from sbt.std.KeyMacro + def enclosingTrees(c: blackbox.Context): Seq[c.Tree] = + c.asInstanceOf[reflect.macros.runtime.Context] + .callsiteTyper + .context + .enclosingContextChain + .map(_.tree.asInstanceOf[c.Tree]) + + def scalafixProjectImpl(c: blackbox.Context): c.Expr[ScalafixProject] = { + import c.universe._ + + val enclosingValName = definingValName( + c, + methodName => + s"""$methodName must be directly assigned to a val, such as `val x = $methodName`. Alternatively, you can use `org.typelevel.sbt.ScalafixProject.apply`""" + ) + + val name = c.Expr[String](Literal(Constant(enclosingValName))) + + reify { ScalafixProject(name.splice) } + } +} diff --git a/scalafix/src/main/scala/org/typelevel/sbt/TypelevelScalafixPlugin.scala b/scalafix/src/main/scala/org/typelevel/sbt/TypelevelScalafixPlugin.scala index b5b794f2..7358e7cd 100644 --- a/scalafix/src/main/scala/org/typelevel/sbt/TypelevelScalafixPlugin.scala +++ b/scalafix/src/main/scala/org/typelevel/sbt/TypelevelScalafixPlugin.scala @@ -19,6 +19,8 @@ package org.typelevel.sbt import sbt._ import scalafix.sbt.ScalafixPlugin +import scala.language.experimental.macros + import Keys._ import ScalafixPlugin.autoImport._ @@ -32,6 +34,8 @@ object TypelevelScalafixPlugin extends AutoPlugin { val tlTypelevelScalafixVersion = settingKey[Option[String]]( "The version of typelevel-scalafix to add to the scalafix dependency classpath, or None to omit the dependency entirely." ) + + def tlScalafixProject: ScalafixProject = macro ScalafixProjectMacros.scalafixProjectImpl } import autoImport._