项目作者: tkroman

项目描述 :
purity enforcer
高级语言: Scala
项目地址: git://github.com/tkroman/puree.git
创建时间: 2019-05-26T21:54:40Z
项目社区:https://github.com/tkroman/puree

开源协议:MIT License

下载


puree

A Scala compiler plugin to warn about unused effects

CircleCI

sbt setup

  1. lazy val pureeV = "0.0.10"
  2. libraryDependencies ++= Seq(
  3. compilerPlugin("com.tkroman" %% "puree" % pureeV),
  4. "com.tkroman" %% "puree-api" % pureeV % Provided
  5. )
  6. // very desirable
  7. scalacOptions ++= Seq(
  8. "-Ywarn-unused:implicits",
  9. "-Ywarn-unused:imports",
  10. "-Ywarn-unused:locals",
  11. "-Ywarn-unused:params",
  12. "-Ywarn-unused:patvars",
  13. "-Ywarn-unused:privates",
  14. "-Werror",
  15. "-Ywarn-value-discard",
  16. )

Disabling Puree selectively

We also ship the puree-api package which provides an @intended annotation
that users can use whenever they want to disable checking for a chunk of code.

Note: @intended also takes optional explanation argument.

  1. import com.tkroman.puree.annotation.intended
  2. @intended
  3. class GoingDirty {
  4. def f(): Future[Int] = Future(1)
  5. def g(): Int = {
  6. f() // I mean... you asked for it
  7. 1
  8. }
  9. }

This will compile fine.

Another realistic usecase is builders:

  1. import com.tkroman.puree.annotation.intended
  2. val buf = List.newBuilder[Byte]
  3. (buf += 0xf.toByte): @intended("not calling .result() here")
  4. buf.result()

In future some of these use-cases may be heuristically solved by the library
but at this point we prefer to remain as flexible as possible.

Strictness configuration

Puree supports (currently) 3 strictness levels:

  • off: Every check is disabled. Same as removing the plugin completely.
  • effects: Only F[_*] checks are performed
  • strict: Any non-unit expression that is not in the return position
    (i.e. is not the last statement of the enclosing expression) is considered “unused” value.
    This can be pretty hard on most code so should be enabled at one’s own discretion.

Default level is effects. To customize, use one of the following flags:

  1. scalacOptions += Seq("-P:puree:level:off")
  2. scalacOptions += Seq("-P:puree:level:effects")
  3. scalacOptions += Seq("-P:puree:level:strict")

Fine-grained control

It is possible to override behavior for individual methods, which is useful
when users want to override some behavior on a system-wide level without
individual suppression via @intended.

If there is a puree-settings file on a compilation classpath,
fine-grained settings will be read from it. File format:

  1. [off]
  2. foo.bar.Baz.::=
  3. [effect]
  4. foo.bar.Quux.methodName
  5. [strict]
  6. foo.bar.Quack.::

Each section is optional.

Motivation: e.g. scala.collection.mutable.Builder.+= method returns
this.type for each builder instance, and since Builder is an F[_, _],
code like

  1. val buf = List.newBuilder[Int]
  2. buf += 1
  3. buf.result()

will be flagged as suspicious. To avoid this, just configure puree with this:

  1. [off]
  2. scala.collection.mutable.Builder.+=

Subtyping checks are performed as expected, so e.g. since Builder is a subtype
of Growable, a warning will not be raised on Builder instances invoking +=
if Growable.+=‘s level is set to Off in settings.

It’s possible to always warn on select methods even if a global level is Off:

  1. [strict]
  2. scala.collection.mutable.Builder.+=

means that += invocations will aways trigger warnings.

Why

Effects

In essence, we say that effect is everything that is not a simple value, so

  1. val intIsNotAnEffect = 1
  2. val dateIsNotAnEffect = LocalDate.now()
  3. val stringIsNotAnEffect = "no, I'm not"
  4. val optionIsAnEffect = Some(5)
  5. val listIsAnEffect = List(1, 2, 3)
  6. val taskIsAnEffect = Task(println("yes, I am"))
  7. val ioIsAnEffect = IO("me too")
  8. val programsAreEffectsOfSorts: IO[Unit] = completeAppInIO
  9. // I don't want to mention Future, but...

In a pure FP setting, most effects are pure,
i.e. declaring or referring to and effect does not mean
any sort of computation being started.

Hence the idea that if somewhere in your code there is this:

  1. def read: Task[String] = Task(in.read())
  2. def write(str: String) = Task(out.write(str))
  3. val prompt: Task[Int = {
  4. write("Enter a number")
  5. val number = read()
  6. number.flatMap(n => Task.fromTry(Try(n.toInt)))
  7. // use that int
  8. }

you will be surprised by an absence of the prompt string,
which will happen because the write(...) expression
doesn’t actually launch the printing routine.

More than that, in most of the cases a presence
of an unused effectful value alone means it’s likely a bug, a typo
or an omission of sorts. Think of a trivial example:

  1. someCode()
  2. List(1,2,3) // What? Why?
  3. somethingElse()

Normally, scalac will warn you if you use a pure expression in a useless context, e.g

  1. val x = 5
  2. 1 // warning here
  3. val y = 10

But it fails to see more complicated examples:

  1. def f = 1
  2. val x = 1
  3. f // no warning
  4. val y = 2

Scala can’t help you out here because believing that all functions are pure
is not practical in general.
However, if you tru writing your code in a more or less principled way,
most of the time this will be true for almost all effectful values and functions.
Think of it as “If I return an F[_, _*]“, I probably wanted to use it.

This plugin is provided specifically as a way to help you with that.

Notes

Works best if you also enable the following scalac flags:

  1. -Ywarn-unused:implicits
  2. -Ywarn-unused:imports
  3. -Ywarn-unused:locals
  4. -Ywarn-unused:params
  5. -Ywarn-unused:patvars
  6. -Ywarn-unused:privates
  7. -Xfatal-warnings
  8. -Ywarn-value-discard

This plugin does not make an attempt to be too smart, the rules are pretty simple:
if there is an F[_, _*], it’s not assigned to anythings,
is not composed with anythings, and is not a last expression in the block,
it’s considered to be worthy a warning. Making warnings into errors via Xfatal-warnings
takes care of the rest.

We also don’t try taking over other warings, so there are no additional rules.

A more comprehensive set of scalac flags one should almost always enable
if they want to maximize compiler’s help can be found here:
https://tpolecat.github.io/2017/04/25/scalac-flags.html

License

MIT