Scala Analyzer adds context-rich rules and is 30% faster ⚡️

We’ve been constantly improving our Scala Analyzer since its beta version was released 3 months ago. In this update, we’ve added new detection capabilities and improved the overall performance of our Scala Analyzer. Keep reading!

P.S Want to know more about getting started with our Scala Analyzer? All the details are here.

So, what’s new?

We identified and made some crucial improvements to the Analyzer. Result: Analysis runs are now up to 30% faster! We’ll be publishing a detailed blog on this soon.

We’ve also upgraded the detection capabilities and added plenty of new checks. Our Scala Analyzer now has the most number of rules than any other static analysis tool out there!

Here are just a few of the scenarios that we now detect –

SC-W1033 - Potential file handle leak

Calling getLines or mkString directly over fromFile, fromURI, or fromURL leaks the underneath opened handle to file.

// Resource leak
val input =
  Input.VirtualFile(
    pathToFile,
    Source.fromFile(pathToFile).mkString
  )

// Manually closing the file yourself
val fileSource   = Source.fromFile("file.txt")
val fileContents = fileSource.mkString
fileSource.close()

// Using the automatic resource management
Using(Source.fromFile("file.txt")) { reader =>
  val fileContents = reader.mkString
  // process the `fileContents`
}

// Handling multiple resources
Using.Manager { manage =>
  val r1 = manage(new BufferedReader(new FileReader("foo.txt")))
  val r2 = manage(new BufferedReader(new FileReader("bar.txt")))
  // ...
}

SC-R1009 - Replace find() ==/!= None with exists()

find() allows you to check for elements that satisfy the defined condition. However, find() ==/!= None can be effectively replaced with exists().

// Not recommended
if (nums.find(x => x % 5 == 0 && x >= 10) != None) {
  // ...
}

// Recommended
if (nums.exists(x => x % 5 == 0 && x >= 10)) {
  // ...
}

SC-W1035 - Use Scala’s deprecated annotation rather than Java’s

Using Java’s annotation may or may not trigger the deprecated warning correctly.

// incorrect - may not necessarily trigger
@Deprecated(...)
def foo(): Unit = {
  //
}

// Preferred way
@deprecated(...)
def foo(): Unit = {
  //
}

SC-R1000 - Merge detached if conditions

The following if conditions can be merged as – if (x > 0 && y > 0)

if (x > 0) {
  if (y > 0) {
    //
  }
}

SC-R1011 - Consider grouping imports

import scala.collection.immutable.HashSet
import scala.collection.immutable.HashMap

The above imports can be written as –

import scala.collection.immutable.{HashSet, HashMap}

Literal arguments to methods

Passing string literals or boolean parameters to methods may or may not convey the required meaning. Therefore, it is generally suggested that you use named parameters in such scenarios.

// From: https://docs.scala-lang.org/tour/named-arguments.html
// Incorrect way
writeToStream(data, true)

// Preferred way
writeToStream(data, flush=true)

SC-R1006 - Calling head/last over filter

filter allows you to filter and select those elements that satisfy your condition. However, calling head or last immediately on the results may not be a wise idea. Consider the following example:

val firstElement = nums.filter(...).head

The above code throws an exception if there’s no element that satisfies the said condition. A better approach to this situation is to use headOption that returns Option[T].

val firstElement = nums.filter(...).headOption

// Alternately, the analyzer can also suggest the following rewrite
val firstElement = nums.find(...)

SC-W1051 - Using .deep to compare Arrays is deprecated

Comparing 2 Arrays using .deep is deprecated and is not supported beyond Scala version 2.12.

val a = Array(1, 2, 3)
val b = Array(1, 2)

a.deep == b.deep                // Deprecated
a.sameElements(b)               // Relying on native Scala approach
java.util.Arrays.equals(a, b)   // Relying on Java's methods

SC-W1052 - Use .isNaN to check if a Double is NaN

Comparison operators such as == or != do not work when checking against NaN. The preferred way is to use the .isNaN method.

val d = Double.NaN

d == Double.NaN       // evaluates to false
d.isNaN               // evaluates to true

SC-W1054 - Calling .get on Try() throws IllegalArgumentException on Failure

Try() returns either Success or Failure depending on whether the specified operation was a success or not. Therefore, calling .get over Try() risks IllegalArgumentException.

// Not recommended
val returnValue = Try(someMethod()).get

// Recommended
Try(someMethod()) match {
  case Success(value) =>
  case Failure(fail)  =>
}

SC-P1006 - Consider rewriting filter().headOption as find()

While .find behaves in the exact same manner as .filter().headOption, it performs slightly better as it terminates after finding the first element that satisfies your condition while .filter continues to iterate through the entire collection.

// Not recommended
val firstEven = nums.filter(x => x % 2 == 0).headOption

// Recommended
val firstEven = nums.find(x => x % 2 == 0)

SC-R1025 - Consider using .isEmpty or .nonEmpty instead of .size for Lists

Methods such as .size have a complexity of O(n) for Lists. Repeatedly calling such methods can impact the performance of your application. Therefore, it is suggested that you use methods such as .isEmpty or .nonEmpty to check if a list is empty or not rather than relying on .size

// Not recommended
def processElements(elements: List[Int]): Unit = {
  if (elements.size != 0) {
    //
  }
}

// Recommended
def processElements(elements: List[Int]): Unit = {
  if (elements.nonEmpty) {
    //
  }
}

Enhancements and fine tuning

We’ve identified a scenario where the SC-W1024 - NonExhaustiveMatch check was being triggered unnecessarily –

foo match {
  case true  =>
  case false =>
}

Since both the possible cases are handled, the specified check should not have been invoked. A patch has been rolled out to fix this issue.

2 Likes