1 of 94

Toward a Safer Scala

Detect errors earlier with �static analysis

2 of 94

Gallatin Peak

Thanks,

Thomas!

3 of 94

Gallatin Peak

@leifwickland

4 of 94

tinyurl.com/

pnwslint

5 of 94

Why Static Analysis?

  • scalac enables some error-prone code
    • Some Scala features demo well, work poorly
  • Coding standards
    • Without enforcement, only suggestions
    • Executable best practices, rather than Markdown
  • Code review
    • Automate it
    • Works when expert or OSS project leader is out

6 of 94

What You Deserve

  • Better compiler
    • Inferring Any is the root of much WTF
    • Invalid string interpolation references

7 of 94

What You Deserve

  • Better compiler
  • More hygienic code
    • Two spaces, no tabs, no discussion
    • Limits on line, function, and file length

8 of 94

What You Deserve

  • Better compiler
  • More hygienic code
  • Higher quality code
    • When I said no nulls, I meant no nulls
    • Regret every var

9 of 94

What You Deserve

  • Better compiler
  • More hygienic code
  • Higher quality code
  • Fewer bugs
    • Runtime exploding String.format()
    • What is the head of an empty List?

10 of 94

Static Analysis Options

  • IDE-based analysis
  • scalac switches
  • FindBugs
  • Scalastyle
  • Abide
  • WartRemover
  • Linter
  • Scapegoat

11 of 94

IDE-based Options

If not in release build process, it doesn’t exist.

If you build your releases with your IDE, I’m very sorry for you.

12 of 94

Criteria

  • Signal to Noise
    • ROI for errors avoided
    • False positives
  • Suppression
    • Possible
    • Limited Scope
  • Learning curve
    • Build integration
    • Documentation

13 of 94

???

14 of 94

15 of 94

Reference uninitialized value

Good: Scala 2.11 warns

Bad: Only warns on change or clean

Ugly: Can still ship it

16 of 94

Seth Tisue says:

“I believe almost any well-managed project should be using �-Xfatal-warnings

in SI-7529

I agree with Seth.�Is your project well-managed?

17 of 94

scalac switches

Recommendation: Enable fatal warnings

scalacOptions ++= Seq(

"-Xfatal-warnings"

)

18 of 94

Set(1).equals()

19 of 94

Adapted Arguments

Set(1).equals()

Compiles�Always false

20 of 94

List(1,2,3).toSet()

21 of 94

Adapted Arguments

List(1,2,3).toSet()

Compiles�Always false

22 of 94

Adapted Arguments

Good: Deprecated around 2.10

Bad: Deprecation is just a warning

23 of 94

Adapted Arguments

Ugly: Compiler reports�"re-run with -deprecation for details"

Uglier: Enabling -deprecation and -Xfatal-warnings means you can’t use deprecated dependencies as stopgap

24 of 94

scalac switches

Recommend: Strive to enable -deprecation

Caveat: Hassle when upgrading dependencies.

scalacOptions ++= Seq(

"-deprecation",

"-Xfatal-warnings"

)

25 of 94

logger.error(

"Failed to open %s. Error: %d"� .format(file)�)

26 of 94

Wrong number of args to format()

logger.error(

"Failed to open %s. Error: %d"� .format(file)�)

Runtime Exception

27 of 94

logger.error(

"Failed to open $file." + � "Error: $IoError"�)

28 of 94

Oh, you wanted to interpolate that?

logger.error(

"Failed to open $file." + � "Error: $IoError"�)

Mo’ money. Mo’ problems.

29 of 94

String Interpolation With -Xlint

logger.error(

"Failed to open $file." + � "Error: $IoError"�)

Missing interpolator

30 of 94

scalac switches

Recommendation: Enable -Xlint

Bonus: 2.11.4 allows selective -Xlint enabling

scalacOptions ++= Seq(

"-Xlint”,

"-deprecation",

"-Xfatal-warnings"

)

31 of 94

sealed trait ADT

object A extends ADT

object B extends ADT

final val C = 3

val adt = Seq(A,B,C)

32 of 94

sealed trait ADT

object A extends ADT

object B extends ADT

final val C = 3

val adt = Seq(A,B,C)

Inferred Seq[Any]

33 of 94

final val As = Seq(1, 2)

final val Bs = Seq("3", "4")

val crossProduct = As.flatMap { a =>

Bs.flatMap { b => Seq(a, b) }

}

34 of 94

final val As = Seq(1, 2)

final val Bs = Seq("3", "4")

val crossProduct = As.flatMap { a =>

Bs.flatMap { b => Seq(a, b) }

}

Inferred Seq[Any]

35 of 94

Inferring Any With -Xlint

You’re right, scalac! Thank you!

A type was inferred to be `Any`; this may indicate a programming error.

36 of 94

scalac switches

Recommendation: No seriously enable -Xlint

scalacOptions ++= Seq(

"-Xlint”,

"-deprecation",

"-Xfatal-warnings"

)

37 of 94

scalac switches

Signal/Noise

Suppression

Learning Curve

38 of 94

FindBugs

  • Venerable Java static analysis tool
  • findbugs4sbt plugs it into sbt
  • High false positive rate for Scala
    • Baffled by synthetic code

39 of 94

FindBugs

?

?

Signal/Noise

Suppression

Learning Curve

40 of 94

Scalastyle

  • Oriented toward “style”
  • Uses scalariform’s parser
    • Not a scalac plugin
  • sbt plugin for easy integration
    • Executed via scalastyle command
    • Can be tied to compile or test commands
  • Used by Martin O’s Coursera classes
  • Mature, fairly well-documented project

41 of 94

Incrementally configured via XML

<scalastyle>

<check level="error" class="NullChecker" enabled="false">

<check level="error" class="FileLengthChecker" enabled="true">

<parameters>

<parameter name="maxFileLength"><![CDATA[1500]]></parameter>

</parameters>

</check>

</scalastyle>

42 of 94

Incrementally configured via XML

<scalastyle>

<check level="error" class="NullChecker" enabled="true">

<check level="error" class="FileLengthChecker" enabled="true">

<parameters>

<parameter name="maxFileLength"><![CDATA[1500]]></parameter>

</parameters>

</check>

</scalastyle>

43 of 94

Incrementally configured via XML

<scalastyle>

<check level="error" class="NullChecker" enabled="true">

<check level="error" class="FileLengthChecker" enabled="true">

<parameters>

<parameter name="maxFileLength"><![CDATA[750]]></parameter>

</parameters>

</check>

</scalastyle>

44 of 94

Suppress via comments

Consistent with Rule 0.1

// scalastyle:off null

TerribleJavaApi.madeMe(null)

// scalastyle:on null

45 of 94

Suppress via comments

Bad.api(null) // scalastyle:ignore null

46 of 94

  1. Executable coding standard
  2. Ban “Better Java”
  3. Encourage Good Practices

47 of 94

Executable coding standard

  • Line, file, and function length
  • Cyclomatic complexity
  • Argument list length
  • Functions per type
  • Indentation
  • Magic numbers
  • Identifier names match regex
  • Require/ban space near punctuation

48 of 94

Ban “Better Java”

  • Ban null
  • Ban var
  • Ban return
  • Ban while
  • Ban clone() method

49 of 94

Encourage Good Practices

  • Explicit return type for public methods
    • Nested methods require suppression
  • Don’t ship ??? and println
  • Require equals/hashCode if other defined
  • Any equals must override Object.equals
  • Disallow structural types
    • Type lambdas require suppression
  • Simplify boolean expressions

50 of 94

Scalastyle Extensibility

  • Simple: Regexes to find banned patterns
  • Complex: Custom rules are possible but, cumbersome

51 of 94

Scalastyle

  • Rule set feels broad and unfocused
  • Implements rules for both sides of controversies

52 of 94

Scalastyle

Signal/Noise

Suppression

Learning Curve

53 of 94

If your coding standard isn’t automatically and uniformly applied, you don’t have a coding standard.

54 of 94

Abide

  • Very new project
  • 0.1-SNAPSHOT
  • Must build from source. Artifacts not online.
  • github.com/scala/scala-abide
    • Driven by Typesafe
      • Iulian Dragos
      • Adriaan Moors
  • Documentation is work in progress

55 of 94

Rules have mainstream flavor

  • Only bans var in public context
  • Detects when a var instead of val
  • Unused locals detection. WANT!
    • Local vars, defs
    • Private class members
    • Method arguments
  • Akka specific rules
    • Avoid capturing unintended context

56 of 94

Abide

?

Signal/Noise

Suppression

Learning Curve

57 of 94

WartRemover

  • Brian McKenna
  • Moving forward fast
  • Accurately and well documented
  • Easy sbt integration
  • github.com/typelevel/wartremover
  • Extensibility is reasonably well documented

58 of 94

val testUsers = Seq(

("Flintstone, Fred", 42),

("Rubble", "Betty", 35)

)

59 of 94

val testUsers = Seq(

("Flintstone, Fred", 42),

("Rubble", "Betty", 35)

)

Inferred Seq[Product]

60 of 94

val testUsers = Seq(

("Flintstone, Fred", 42),

("Rubble", "Betty", 35)

)

Inferred Seq[Product with Serializable]

61 of 94

  • Avoid Problematic Inference
  • Ban “Better Java”
  • Partial Methods which throw

62 of 94

Avoid Problematic Inference

  • Product
  • Serializable
  • Nothing
  • Any

63 of 94

Ban “Better Java”

  • Ban null
  • Ban var
  • Ban return

64 of 94

Partial Methods which throw

  • List.head, .tail, .last
  • Option.get
  • scala.util.Either.get

65 of 94

wartremoverErrors ++= Warts.all

66 of 94

wartremoverErrors ++= Seq(

Wart.Any,

Wart.Nothing,

Wart.Serializable,

Wart.Product

)

67 of 94

wartremoverExcluded ++= Seq(

"com.bad.package",

"com.good.DirtyClass"

)

68 of 94

WartRemover

Signal/Noise

Suppression

Learning Curve

69 of 94

WartRemover

  • Focused and opinionated
  • Recommendation: Best suited to
    1. Greenfield, or
    2. Already clean projects, or
    3. Writing the typelevel-y-est Scala

70 of 94

def f(s: String) = {

s.split(':') match {

case Array(k,v) => k -> v

case a => error("bad: " + a)

}

}

71 of 94

def f(s: String) = {

s.split(':') match {

case Array(k,v) => k -> v

case a => error("bad: " + a)

}

}

Array.toString()�bad: [LString;@6f89341e

72 of 94

def g(l: List) = {

if (l.length == 0) 0

else l.reduce(...)

}

73 of 94

def g(l: List) = {

if (l.length == 0) 0

else l.reduce(...)

}

List.length is O(n)

Use isEmpty()

74 of 94

Linter

  • Many years of many forks
    • Jorge Ortiz: com.foursquare.lint in 2012
    • Matic Potočnik (@HairyFotr): current fork
  • Documentation is spotty
  • scalac plugin without sbt plugin
  • Configured via scalac switches
  • No suppression mechanism

75 of 94

Implements 50% more rules than Scalastyle

  • Guess what they do based on name
  • Tend toward more traditional linter-style

76 of 94

Linter found

  • String.format() call with mismatched args
  • Unused function argument
  • aList.length == 0 <- use .isEmpty
  • Using string interpolation unnecessarily
  • Calling .toString on Array
  • Close scala.io.Source
  • Inferring Nothing

77 of 94

Linter Also Found

  • Casting instead of .toByte
  • Calling .toSeq on a Seq
  • Calling .toString on a String
  • Calling .toList on a List
  • Calling .toMap on a Map
  • Use JavaConversions, not JavaConverters

78 of 94

Linter

  • I really like it but it’s iffy to recommend
  • Incomplete docs
  • 0.1-SNAPSHOT
  • Definitely has value; identified real bugs
  • No false positives
  • Wish: short list of highest ROI checks
  • Wish: suppression
  • Wish: sbt plugin

79 of 94

Linter

Signal/Noise

Suppression

Learning Curve

80 of 94

seq.find(_ > 42).isDefined

81 of 94

seq.find(_ > 42).isDefined

seq.exists(_ > 42)

82 of 94

seq.exists(_ == "the one")

83 of 94

seq.exists(_ == "the one")

seq.contains("the one")

84 of 94

Scapegoat

  • Stephen Samuel
  • Less than a year old
  • Releasing prolifically
  • Churning a fair bit
  • sbt plugin makes it easy to get started
  • Fairly good docs. A little behind the code.
  • File level, path regex based suppression

85 of 94

WartRemover �++ �Linter

++�Scalastyle

86 of 94

.find(...).isDefined -> .exists(...)

Lonely sealed class

Match instead of partial function

Duplicated import

.length == 0

var could be a val

87 of 94

Unused method parameter

Use .contains(y) instead of� .exists(_ == y)

88 of 94

What I dislike

  • Overwhelming. Enables 100+ rules by default
  • Somewhat unclear how to disable rule
  • Bans final case class
  • Bans wildcard imports. Bedazzling?
  • At least 5 different classes of false positive

89 of 94

Scapegoat

Signal/Noise

Suppression

Learning Curve

90 of 94

S/N

Sup

LC

?

?

scalac switches

FindBugs

Scalastyle

Abide

WartRemover

Linter

Scapegoat

?

91 of 94

Greenfield

  1. scalac switches
    1. Adapted Rob Norris’ post
  2. Scalastyle coding standard
  3. WartRemover for inference and partial

Protip: Skeleton Project

92 of 94

Existing Project

Start small; tighten screws

  1. Start with all rules disabled
  2. Make linter gate in CI
  3. Pick a rule team agrees on
  4. Clean up code. Suppress.
  5. Enable rule as error
  6. Goto 3

93 of 94

TL;DR

scalacOptions ++= Seq(

"-Xlint”,

"-deprecation",

"-Xfatal-warnings"

)

94 of 94

Questions?