Note: This page describes the latest release of SuperSafe 1.1, 1.1.12. The older version of SuperSafe, 1.0.6, is described in Artima SuperSafe 1.0 User Guide.
Artima SuperSafe is a Scala compiler plugin
that enforces a safer subset of Scala at
compile time by triggering compiler errors for problematic expressions that would otherwise compile.
For example, although the following two expressions will always be false
and
almost certainly represent bugs, the Scala compiler allows them:
scala> "one" == 1 res0: Boolean = false scala> List(1, 2, 3).contains("one") res1: Boolean = false
With the Artima SuperSafe plugin installed, those expressions will not compile:
scala> "one" == 1 <console>:8: error: [Artima SuperSafe] Values of type String and Int may not be compared with the == operator. If you really want to compare them for equality, configure Artima SuperSafe to allow those types to be compared for equality. For more information on this kind of error, see: https://www.artima.com/supersafe_user_guide.html#safer-equality "one" == 1 ^ scala> List(1, 2, 3).contains("one") <console>:8: error: [Artima SuperSafe] A value of type String cannot be passed to contains method on List[Int]. If you really want to do this, configure Artima SuperSafe to allow those types to be compared for equality. For more information on this kind of error, see: https://www.artima.com/supersafe_user_guide.html#safer-equality List(1, 2, 3).contains("one") ^
The purpose of Artima SuperSafe is to enable you to find such problems sooner—at compile time rather than through testing—and to ensure such problems never make it into your final product.
Starting with release 1.1.0, Artima SuperSafe includes Scalactic and ScalaTest 3.x support that is free for Scalactic and ScalaTest users.
You can just install Artima SuperSafe and enjoy safe Scalactic and ScalaTest without purchasing a license.
Artima SuperSafe is designed to be run any time you run the Scala compiler. Thus
to install Artima SuperSafe, you'll usually want to add it to your build. The easiest way
to do that is to add the SuperSafe Jar as a dependency to be automatically downloaded from
the Artima Maven Repository, then place a license key file into a directory
named .supersafe
into the home directory of each developer.
If you are using sbt
as your build tool, you can install SuperSafe in three easy steps.
1. Add the Artima Maven Repository as a resolver in ~/.sbt/1.0/global.sbt (or ~/.sbt/0.13/global.sbt if you're using SBT 0.13.x), like this:
resolvers += "Artima Maven Repository" at "https://repo.artima.com/releases"
2. Add the following line to your project/plugins.sbt:
addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.12")
3. If you have purchased a license (required for full feature), copy your downloaded license file into a directory named .supersafe
in your home directory.
If you are using Maven as your build tool, you can install SuperSafe in three easy steps.
1. Add the Artima Maven Repository to your pom.xml
, like this:
<repositories> <repository> <id>artima</id> <name>Artima Maven Repository</name> <url>https://repo.artima.com/releases</url> </repository> </repositories>
2. Add the compiler plugin to your pom.xml
, like this:
<plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <configuration> <compilerPlugins> <compilerPlugin> <groupId>com.artima.supersafe</groupId> <artifactId>supersafe_2.13.15</artifactId> <version>1.1.12</version> </compilerPlugin> </compilerPlugins> </configuration> <executions> ... </executions> </plugin>
Note: You need to use the exact Scala version in the artifactId, because compiler plugin depends on compiler API that's not binary compatible between Scala minor releases.
3. If you have purchased a license (required for full feature), copy your downloaded license file into a directory named .supersafe
in your home directory.
Artima SuperSafe is designed to enforce a policy that you configure for your project through a single configuration file that is shared by all the developers on the project. Thus you can see the SuperSafe configuration file as a part of your build.
Once you have a configuration file, you'll need to tell Artima SuperSafe about it by specifying the following scalac
command
line option:
-P:artima-supersafe:config-file:<path-to-config-file>
Note: "<path-to-config-file>
" is
the relative path to your configuration file from the root directory of your build.
If your configuration file is named supersafe.cfg
and is located in the project
folder, you
would add the following to your sbt build file:
scalacOptions += "-P:artima-supersafe:config-file:project/supersafe.cfg"
If your configuration file is named supersafe.cfg
and located in the root directory of your project, you would add the following in your pom.xml
file:
<plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <configuration> <compilerPlugins> <compilerPlugin> <groupId>com.artima</groupId> <artifactId>supersafe_2.13.15</artifactId> <version>1.1.12</version> </compilerPlugin> </compilerPlugins> <args> <arg>-P:artima-supersafe:config-file:supersafe.cfg</arg> </args> </configuration> <executions> ... </executions> </plugin>
In Artima SuperSafe's configuration file, the first non-empty line must be a declaration of the version of the configuration file format:
version 1
If your configuration file does not contain the above text as the first non-empty line, Artima SuperSafe will print out a warning and all configurations in the file will be ignored.
After the version line, the configuration may contain configuration rules that must follow the following BNF grammar:
preventRule ::= prevent (comparisonClassName | nestedArray | nestedCooperative | suspiciousInferredType)
cooperativeRule ::= cooperative classNames
suspiciousInferredType ::= suspicious inferred type
nestedArray ::= nested array
nestedCooperative ::= nested cooperative className className
comparisonClassName ::= comparison className
classNames ::= className \{className\}.
className ::= """([\p{L}_$][\p{L}\p{N}_$]*\.)*[\p{L}_$][\p{L}\p{N}_$]*""".r
cooperative ::= "cooperative"
prevent ::= "prevent"
nested ::= "nested"
comparison ::= "comparison"
suspicious ::= "suspicious"
inferred ::= "inferred"
type ::= "type"
array ::= "array"
scalactic ::= "scalactic"
Each rule must appear wholly on its own line. Here is an example configuration file:
cooperative com.test.Apple com.test.Orange
prevent comparison com.test.Fruit
prevent nested array
prevent suspicious inferred type
You can also add comments in the configuration file. Just start the line with '#' and Artima SuperSafe will treat that line as a comment line:
# this is a comment line
prevent nested array
The purpose and behavior of each configuration rule is explained in the next section, Features.
By default (i.e., in the absence of a configuration file), Artima SuperSafe will analyze and possibly generate compiler errors for the following kinds of expressions:
==
for all types!=
for all typescontains
for GenSeq
, Array
, and Option
*indexOf
for GenSeq
and Array
*lastIndexOf
for GenSeq
and Array
*indexOfSlice
for GenSeq
and Array
*lastIndexOfSlice
for GenSeq
and Array
*===
for all types!==
for all typesshould equal
matcher for all typesshouldEqual
matcher for all typesshould be
matcher for all typesshouldBe
matcher for all typesshould not equal
matcher for all typesshould not be
matcher for all typesshouldNot equal
matcher for all typesshouldNot be
matcher for all typesmust equal
matcher for all typesmustEqual
matcher for all typesmust be
matcher for all typesmustBe
matcher for all typesmust not equal
matcher for all typesmust not be
matcher for all typesmustNot equal
matcher for all typesmustNot be
matcher for all typescontain
matchers for GenSeq
, Array
, and Option
*Collection rework in Scala 2.13 now catch the problem already.
For equality comparisons with ==
, !=
, Scalactic's ===
and !==
, ScalaTest's should equal
, shouldEqual
, should be
, shouldBe
, should not equal
, should not be
, shouldNot equal
, shouldNot be
matcher, Artima SuperSafe will by default generate a compiler error if all of the following are true:
scala.collection.GenSeq[T]
.scala.collection.GenSet[T]
.scala.collection.GenMap[T]
.
For containership comparisons with contains
, indexOf
, lastIndexOf
,
indexOfSlice
lastIndexOfSlice
and ScalaTest's contain matchers, Artima SuperSafe will use the same test
described above, but between
the element type of the LHS "container" type and the RHS type.
AnyVal
and Null
Instances of AnyVal
, like Int
, Long
, and Char
, are value classes that should never be compared to null
. This is an example of the LHS and RHS not being in a subtype/supertype relationship, since the Null
, the type of null
, is not a subtype of AnyVal
. Artima SuperSafe will, therefore, generate a compiler error for any comparison of an AnyVal
with null
.
Types that cooperate in equality comparisons such that all the requirements of the equals
contract on java.lang.Object
are met, are "cooperative types." As an example, Scala's BigInt
and BigDecimal
types cooperate and can be compared directly for equality:
scala> BigInt(42) == BigDecimal(42)
res11: Boolean = true
scala> BigDecimal(42) == BigInt(42)
res12: Boolean = true
By default, Artima SuperSafe allows equality comparisons between the following cooperative types:
scala.Int
scala.Long
scala.Double
scala.Short
scala.Byte
scala.Float
scala.Char
scala.math.BigInt
scala.math.BigDecimal
java.lang.Character
java.lang.Double
java.lang.Integer
java.lang.Long
java.lang.Byte
java.lang.Float
java.lang.Short
scala.Boolean
java.lang.Boolean
scala.concurrent.duration.FiniteDuration
scala.concurrent.duration.Duration.Infinite
You can enable equality comparisons between your own cooperative types by adding a rule to the configuration file. For example, consider the following classes:
case class Apple(i: Int) {
override def hashCode = i.hashCode
override def equals(o: Any) =
o match {
case Apple(j) => i == j
case Orange(j) => i == j
case _ => false
}
}
case class Orange(i: Int) {
override def hashCode = i.hashCode
override def equals(o: Any) =
o match {
case Orange(j) => i == j
case Apple(j) => i == j
case _ => false
}
}
By default if you say Apple(1) == Orange(1)
Artima SuperSafe will generate a compiler error, but you can add a rule in the configuration file to declare Apple
and Orange
to be cooperative:
cooperative Apple Orange
With this rule, Artima SuperSafe will allow equality comparisons between Apple
s and Orange
s to compile. Note that the type names must be fully qualified.
Artima SuperSafe analyzes nested types involved in equality comparisons, not just the top-level types. For example, Artima SuperSafe will generate a compiler error for the following expression:
List[Int] == List[String]
Array
comparisonArtima SuperSafe will generate a compiler error when it sees either the LHS or RHS is Array
, because comparing Array
with ==
or !=
is not obviously correct. When both the LHS and RHS are Array
, Artima SuperSafe will give a compiler error message that suggests you either call .deep
on both the LHS and RHS before comparing with ==
or !=
, or use eq
or ne
if you actually intended to compare whether the references refer to the exact same Array
instance.
By default, Artima SuperSafe will allow comparisons in which Array
s are nested inside non-Array
s, such as List[Array[Int]] == List[Array[Int]]
. The reason is that this cannot be fixed with a simple addition of .deep
invocations, but instead requires a mapping transformation of both lists. If you wish to disallow Array
s nested inside non-Array
s, you can add the following rule to your Artima SuperSafe configuration file:
prevent nested array
Artima SuperSafe will allow comparison between Array
and GenSeq
at top level when used with Scalactic's ===
and !==
, as well as ScalaTest's matchers, because Scalactic and ScalaTest will automatically invoke .deep
when Array
is detected. This exception does not apply to nested type though, so if prevent nested array
rule is enabled Artima SuperSafe will still raise error for the following expression:
List(Array(1)) === List(Array(1))
Some types do not lend themselves to a structural equality comparison. For example, to compare two function types for structural equality would usually require that all possible inputs be passed to both functions, ensuring they return the same result. This is not usually practical, so Scala's FunctionN
traits do not override equals at all. As a result, ==
comparisons between functions check for reference equality:
scala> val f = (i: Int) => i + 1
f: Int => Int = <function1>
scala> val g = (i: Int) => i + 1
g: Int => Int = <function1>
scala> f == f
res2: Boolean = true
scala> f == g
res3: Boolean = false
If you wish to disallow such comparisons altogether, you can enter the following in your Artima SuperSafe configuration file:
prevent comparison scala.Function1
That above configuration rule will disable any ==
or !=
top-level comparison involving Function1
, but still allow Function
s to participate in equality comparisons when nested inside other types, such as List(f, f) == List(f, g)
. To disallow nested comparisons as well, add the nested
keyword at the end of the prevent
comparison
rule:
prevent comparison scala.Function1 nested
Note that only the exact type mentioned in the prevent
comparison
rule will be affected. Any sub-type of the mentioned type can still be used in an equality comparison. Thus, if you wanted to disallow Any
to ever appear in an equality comparison, for example, you can place this line in your configuration file:
prevent comparison Any nested
Unfortunately, Java collections of cooperative numeric types can violate the equals
contract. Here's an example:
scala> val ints = new java.util.LinkedList[Int]
ints: java.util.LinkedList[Int] = []
scala> val bigints = new java.util.LinkedList[BigInt]
bigints: java.util.LinkedList[BigInt] = []
scala> ints.add(1)
res0: Boolean = true
scala> bigints.add(1)
res1: Boolean = true
scala> ints == bigints
res2: Boolean = false
scala> bigints == ints
res3: Boolean = true
The equality comparisons are asymmetric because ints == bigints
is not equal to bigints == ints
. Artima SuperSafe can be configured to catch this type of mis-behavior via the prevent
nested
cooperative
rule. By default, Artima SuperSafe has the following rules enabled:
prevent nested cooperative java.util.AbstractList JavaNumericTypes
prevent nested cooperative java.util.AbstractSet JavaNumericTypes
This default exists because java.util.AbstractList's equals method uses the equals
method to perform equality checking, and the equals
method on Java numeric types, such as java.lang.Integer, will return true
only when the other object is also a java.lang.Integer
that wraps the same value. In other words, the Java wrapper types only cooperate for ==
and !=
comparisons, not when equals
is called directly.
JavaNumericTypes
will automatically include all Java primitive numeric type wrappers. Thus the following rule:
prevent nested cooperative java.util.AbstractList JavaNumericTypes
is equivalent to the specifying the following seven rules:
prevent nested cooperative java.util.AbstractList java.lang.Integer
prevent nested cooperative java.util.AbstractList java.lang.Long
prevent nested cooperative java.util.AbstractList java.lang.Double
prevent nested cooperative java.util.AbstractList java.lang.Short
prevent nested cooperative java.util.AbstractList java.lang.Byte
prevent nested cooperative java.util.AbstractList java.lang.Float
prevent nested cooperative java.util.AbstractList java.lang.Character
equals
In Scala, ==
should be used in place of any equals
, because ==
treats null
on the LHS gracefully whereas equals
throws NullPointerException
:
scala> val a = null
a: Null = null
scala> a == "test"
res0: Boolean = false
scala> a equals "test"
java.lang.NullPointerException
... 33 elided
In addition, ==
implements Scala's cooperative equality of numeric types, whereas equals
does not:
scala> val a = 1
a: Int = 1
scala> val b = BigInt(1)
b: scala.math.BigInt = 1
scala> a == b
res0: Boolean = true
scala> b == a
res1: Boolean = true
scala> a equals b
res2: Boolean = false
scala> b equals a
res3: Boolean = true
Artima SuperSafe will generate a compiler error whenever it sees the equals
method is being used, with the exception that calls to super.equals
are still allowed.
When the inferred type of a val
, def
, or var
consists only of one or more of Any
, AnyVal
, AnyRef
, Product
, or Serializable
, it often indicates an unintended widening. Here's an example in which a Tuple3
is accidentally included in a list intended for Tuple2
s:
scala> List((1, 2), (3, 4), (5, 6, 6))
res0: List[Product with Serializable] = List((1,2), (3,4), (5,6,6))
If you desire compiler errors in such cases, you add the following line into Artima SuperSafe configuration file:
prevent suspicious inferred type
If you have turned this option on and do have code that correctly returns an intersection of the above top-level types, you can just add in the type annotation to get rid of the Artima SuperSafe error:
val a: Any = functionReturningAny()
Option
, Some
, None
Artima SuperSafe will analyze contains
method invocations on scala.Option
and scala.Some
. Artima SuperSafe will generate a compiler error whenever the contains
method is invoked on None
, because it will always result in false
. Please note that the contains
method is only added to Option
in Scala 2.11.
String
Artima SuperSafe will analyze method invocations on String
's contains
, indexOf
, and lastIndexOf
, allowing only Char
, String
, CharSequence
or Int
(representing a Char
) to be passed in. For indexOfSlice
and lastIndexOfSlice
, it allows String
, GenSeq[Char]
, or Int
only.
Artima SuperSafe is licensed under the
Artima SuperSafe Software License
Agreement.