The Artima Developer Community

Artima SuperSafe™ 1.1 User Guide

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.

1. Overview

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.

2. Installation

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.

Artima SuperSafe supports the following Scala versions:

2.1 Installation in sbt

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.

Notes:
  1. SuperSafe SBT plugin will automatically detect the scala version used by your project and download the correct build of the SuperSafe compiler plugin for your project.
  2. You can skip step 3 above if you are using features available in community edition only.

2.2 Installation in Maven

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.13</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.

Notes:
  1. SuperSafe SBT plugin will automatically detect the scala version used by your project and download the correct build of the SuperSafe compiler plugin for your project.
  2. You can skip step 3 above if you are using features available in community edition only.

3. Configuration file

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.

3.1 Specifying the configuration file in sbt

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"

3.2 Specifying the configuration file in Maven

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.13</artifactId>
                    <version>1.1.12</version>
                </compilerPlugin>
            </compilerPlugins>
            <args>
                <arg>-P:artima-supersafe:config-file:supersafe.cfg</arg>
            </args>
        </configuration>
        <executions>
            ...
        </executions>
    </plugin>

3.3 Configuration file grammar

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.

4. Features

4.1 Safer equality comparisons

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:

*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:

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.

4.2 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.

4.3 Cooperative types

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:

Numeric types

Boolean types

FiniteDuration & 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 Apples and Oranges to compile. Note that the type names must be fully qualified.

4.4 Nested type checking

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]

4.5 Array comparison

Artima 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 Arrays are nested inside non-Arrays, 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 Arrays nested inside non-Arrays, 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))

4.6 Preventing a type from being used in comparison

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 Functions 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

4.7 Prevent nested cooperative

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

4.8 Prevent 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.

4.9 Prevent suspicious inferred type

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 Tuple2s:

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()

4.10 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.

4.11 Safer 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.




Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use