1. sbt Coding Guideline

sbt Coding Guideline 

This page discusses the coding style and other guidelines for sbt 1.0.

General goal 

sbt 1.0 will primarily target Scala 2.12. We will cross-build against Scala 2.10.

Clean up old deprecation 

Before 1.0 is release, we should clean up deprecations.

Aim for zero warnings (except deprecation) 

On Scala 2.12 we should aim for zero warnings. One exception may be deprecation if it’s required for cross-building.

Documentation 

It is often useful to start with the Scaladoc before fleshing out a trait/class implementation by forcing you to consider the need for its existence.

All newly introduced public traits and classes and, to a lesser extent, functions and methods, should have Scaladoc. A significant amount of existing sbt code lacks documentation and we need to repair this situation over time. If you see an opportunity to add some documentation, or improve existing documentation then this will also help.

Package level documentation is a great place to describe how various components interact, so please consider adding/enhancing that where possible.

For more information on good Scaladoc style, please refer to the Scaladoc Style Guide

Modular design 

Aim small 

The fewer methods we can expose to the build user, the easier sbt becomes to maintain.

Public APIs should be coded against “interfaces“ 

Code against interfaces.

Hide implementation details 

The implementation details should be hidden behind sbt.internal.x packages, where x could be the name of the main package (like io).

Less interdependence 

Independent modules with fewer dependent libraries are easier to reuse.

Hide external classes 

Avoid exposing external classes in the API, except for standard Scala and Java classes.

Hide internal modules 

A module may be declared internal if it has no use to the public.

Compiler flags 

-encoding utf8
-deprecation
-feature
-unchecked
-Xlint
-language:higherKinds
-language:implicitConversions
-Xfuture
-Yinline-warnings
-Yno-adapted-args
-Ywarn-dead-code
-Ywarn-numeric-widen
-Ywarn-value-discard
-Xfatal-warnings

The -Xfatal-warnings may be removed if there are unavoidable warnings.

Package name and organization name 

Use the package name appended with the layer name, such as sbt.io for IO layer. The organization name for published artifacts should remain org.scala-sbt.

Binary resiliency 

A good overview on the topic of binary resiliency is Josh's 2012 talk on Binary resiliency. The guideline here applies mostly to publicly exposed APIs.

MiMa 

Use MiMa.

Public traits should contain def declarations only 

  • val or var in a trait results in code generated at subclass and in the artificial Foo$class.$init$
  • lazy val results in code generated at subclass

Abstract classes are also useful 

To trait, or not to trait?. Abstract classes are less flexible than traits, but traits pose more problems for binary compatibility. Abstract classes also have better Java interoperability.

Seal traits and abstract classes 

If there’s no need to keep a class open, seal it.

Finalize the leaf classes 

If there’s no need to keep a class open, finalize it.

Typeclass and subclass inheritance 

The typeclass pattern with pure traits might ease maintaining binary compatibility more so than subclassing.

Avoid case classes, use sbt-datatype 

Case classes involve code generation that makes it harder to maintain binary compatibility over time.

Prefer method overloading over default parameter values 

Default parameter values are effectively code generation, which makes them difficult to maintain.

Other public API matters 

Here are other guidelines about the sbt public API.

Avoid Stringly-typed programming 

Define datatypes.

Avoid overuse of def apply 

def apply should be reserved for factory methods in a companion object that returns type T.

Use specific datatypes (Vector, List, or Array), rather than Seq 

scala.Seq is scala.collection.Seq, which is not immutable. Default to Vector. Use List if constant prepending is needed. Use Array if Java interoperability is needed. Note that using mutable collections is perfectly fine within the implementation.

Avoid calling toSeq or anything with side-effects on Set 

Set is fine if you stick to set operations, like contains and subsetOf. More often than not, toSeq is called explicitly or implicitly, or some side-effecting method is called from map. This introduces non-determinism to the code.

Avoid calling toSeq on Map 

Same as above. This will introduce non-determinism.

Avoid functions and tuples in the signature, if Java interoperability is needed 

Instead of functions and tuples, turn them into a trait. This applies where interoperability is a concern, like implementing incremental compilation.

Style matters 

Use scalafmt 

sbt-houserules comes with scalafmt for formatting source code consistently.

Avoid procedure syntax 

Declare an explicit Unit return.

Define instances of typeclasses in their companion objects, when possible 

This style is encouraged:

final class FooID {}
object FooID {
  implicit val fooIdPicklerUnpicker: PicklerUnpickler[FooID] = ???
}

Implicit conversions for syntax (enrich-my-library pattern) should be imported 

Avoid defining implicit converters in companion objects and package objects.

Suppose the IO module introduces a URL enrichment called RichURI, and LibraryManagement introduces a String enrichment called GroupID (for ModuleID syntax). These implicit conversions should be defined in an object named syntax in the respective package:

package sbt.io

object syntax {
  implicit def uriToRichURI(uri: URI): RichURI = new RichURI(uri)
}

When all the layers are available, the sbt package should also define an object called syntax which forwards implicit conversions from all the layers:

package sbt

object syntax {
  implicit def uriToRichURI(uri: URI): io.RichURI = io.syntax.uriToRichURI(uri)
  ....
}