travisbrown / simulacrum-scalafix

Simulacrum as Scalafix rules

Version Matrix

Simulacrum Scalafix experiment

Build status Coverage status

This project is an experiment in rewriting Typelevel Simulacrum as a set of Scalafix rules. It's currently a rough proof-of-concept, but it is able to replace Simulacrum in the Cats repository without breaking any tests or binary-compatibility checks.

Please see this Cats issue for discussion about this experiment.

Simulacrum rules

Simulacrum 1's @typeclass macro annotation adds three kinds of boilerplate, which this project factors into three Scalafix rules:

  • AddSerializable: This rule adds extends Serializable to the root type classes in a type class hierarchy.
  • AddImplicitNotFound: This rule adds custom @implicitNotFound annotations for type classes.
  • TypeClassSupport: This rule adds a summoner method and syntax method support to the type class companion object.

Dotty compatibility rules

This repo currently includes a few miscellaneous Scalafix rules that may be useful for experimenting with Dotty cross-compilation:

  • ExpandTypeLambdas: This rule rewrites (a large subset of) kind-projector's type lambda syntax to a representation that currently works on both Scala 2 and Dotty.
  • ExpandPolymorphicLambdas: This rule rewrites kind-projector's polymorphic lambda values to explicit anonymous class instantiations that work on both Scala 2 and Dotty.
  • ParenthesizeLambdaParamWithType: This rule parenthesizes lambda parameters with type annotations as needed.

The kind-projector-related Expand rules are not intended to be a long-term solution. The issue is that right now Dotty has its own syntax for type lambdas and polymorphic function values, and it doesn't support kind-projector's syntax (despite the existence of a -Ykind-projector compiler option, which currently doesn't actually do anything), while kind-projector doesn't support Dotty's syntax on Scala 2.

If you want to cross-compile for Scala 2 and Dotty right now, you need to use an extremely verbose encoding of type lambdas and polymorphic function values, which is what the rules in this repo target. In the future this won't be necessary, either because Dotty will add support for kind-projector syntax via its -Ykind-projector flag, or because kind-projector will add support for Dotty syntax, or some of both (or something else entirely). In any case you almost certainly don't want to use the verbose encoding you'll get from these Expand rules outside experimental branches.

Building Cats without macro annotations

It's not currently possible to build Cats with Dotty because Simulacrum 1 uses macro annotations, which Dotty doesn't support. The goal of this project is to change that by providing a non-macro-annotation-based version of Simulacrum.

I have a Cats branch that demonstrates how these Scalafix rules work. You can follow along with the following steps:

  1. Clone this repo and run sbt +publishLocal to publish the annotations and Scalafix rules locally.
  2. Check out the current master branch on Cats (in my case this commit).
  3. If #3186, #3187, #3190, and #3191 aren't yet merged, cherry-pick their commits.
  4. Add Scalafix and the locally-published Scalafix rules to the Cats build and remove Simulacrum 1, Macro Paradise, etc. This takes about a dozen lines of configuration.
  5. Add @noop annotations to FlatMap#ifM, Functor#ifF, and Monad#whileM and whileM_. This is necessary for compatibility because Simulacrum 1 didn't handle these methods.
  6. Open an sbt console in the Cats repo and run the following commands:
    sbt:cats> scalafix AddSerializable
    sbt:cats> scalafix AddImplicitNotFound
    sbt:cats> scalafix TypeClassSupport
    sbt:cats> scalafmtAll
    This will result in some boilerplate being added to the Cats source files:
    50 files changed, 2206 insertions(+), 17 deletions(-)
  7. Run sbt validateJVM to verify that tests and binary-compatibility checks pass after the change (and sbt ++2.13.1 buildJVM if you want to check Scala 2.13 as well).

Cross-building cats-core on Dotty

You can add Dotty cross-building with a few additional steps:

  1. Add build configuration for Dotty. This is a net couple dozen lines.
  2. Move the one macro definition in cats-core into a Scala 2-specific source directory tree.
  3. Expand type lambdas and polymorphic function value definitions with the following commands:
    sbt:cats> +scalafix ExpandTypeLambdas
    sbt:cats> +scalafix ExpandPolymorphicLambdas
    sbt:cats> +scalafmtAll
    This will result in a pretty big diff. Unlike the similar Simulacrum boilerplate expansion we did above, this kind-projector expansion probably isn't something we'd ever want to mergeā€”it's just a convenient way to try out Dotty cross-building.
  4. Add a couple of casts that Dotty needs for some reason.
  5. Compile on Dotty:
    sbt:cats> ++0.21.0-bin-20191201-65a404f-NIGHTLY
    sbt:cats> coreJVM/compile
    sbt:cats> kernelLawsJVM/compile
    sbt:cats> alleycatsCoreJVM/compile
    These modules should compile without errors (the other laws modules and cats-free will currently fail).


This experimental code is licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License.

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.