Skip to content

Scala compiler plugin for static code analysis

License

Notifications You must be signed in to change notification settings

saeltz/scapegoat

 
 

Repository files navigation

Scapegoat

Codecov Scala Steward badge

Scapegoat is a Scala static code analyzer, which is more colloquially known as a code lint tool or linter. Scapegoat works in a similar vein to Java's FindBugs or checkstyle, or Scala's Scalastyle.

A static code analyzer is a tool that flags suspicious language usage in code. This can include behavior likely to lead to bugs, non-idiomatic usage of a language, or just code that doesn't conform to specified style guidelines.

What's the difference between this project and Scalastyle (or others)?

Scalastyle is a similar linting tool which focuses mostly on enforcing style/code standards. There are no problems in running multiple analysis tools on the same codebase. In fact, it could be beneficial as the total set of possible warnings is the union of the inspections of all the enabled tools. The worst case is that the same warnings might get generated by multiple tools.

Usage

Scapegoat is developed as a scala compiler plugin, which can then be used inside your build tool.

SBT

See: sbt-scapegoat for SBT integration.

Maven

Firstly you need to add scapegoat plugin as a dependency:

<dependency>
    <groupId>com.sksamuel.scapegoat</groupId>
    <artifactId>scalac-scapegoat-plugin_${scala.version}</artifactId>
    <version>1.3.12</version>
</dependency>

Then configure scala-maven-plugin by adding compilerPlugin

<plugin>
    <groupId>net.alchim31.maven</groupId>
    <artifactId>scala-maven-plugin</artifactId>
    <configuration>
        <args>
            <arg>-P:scapegoat:dataDir:./target/scapegoat</arg>
        </args>
        <compilerPlugins>
            <compilerPlugin>
                <groupId>com.sksamuel.scapegoat</groupId>
                <artifactId>scalac-scapegoat-plugin_${scala.binary.version}</artifactId>
                <version>1.4.6</version>
            </compilerPlugin>
        </compilerPlugins>
    </configuration>
</plugin>

The only required parameter is dataDir (where report will be generated):

<arg>-P:scapegoat:dataDir:./target/scapegoat</arg>

You can pass other configuration flags same way, e.g.

<arg>-P:scapegoat:disabledInspections:FinalModifierOnCaseClass</arg>

Note: You may use a separate maven profile, so that the dependency doesn't go to you runtime classpath.

Gradle with a plugin

Use gradle-scapegoat-plugin by @eugene-sy

Gradle - manually

Firstly you need to add scapegoat plugin as a dependency:

dependencies {
  compile 'com.sksamuel.scapegoat:scalac-scapegoat-plugin_2.12.14:1.4.6'
  scalaCompilerPlugin "com.sksamuel.scapegoat:scalac-scapegoat-plugin_2.12.14:1.4.6"
}

Then configure scala-compiler-plugin

configurations {
  scalaCompilerPlugin
}

tasks.withType(ScalaCompile) {
  scalaCompileOptions.additionalParameters = [
    "-Xplugin:" + configurations.scalaCompilerPlugin.asPath,
    "-P:scapegoat:dataDir:" + buildDir + "/scapegoat"
  ]
}

The only required parameter is dataDir (where report will be generated):

"-P:scapegoat:dataDir:" + buildDir + "/scapegoat",

You can pass other configuration flags adding it to the additionalParameters list, e.g.

"-P:scapegoat:disabledInspections:FinalModifierOnCaseClass"

Full list of compiler flags

Flag Parameters Required
-P:scapegoat:dataDir: Path to reports directory for the plugin. true
-P:scapegoat:disabledInspections: Colon separated list of disabled inspections (defaults to none). false
-P:scapegoat:enabledInspections: Colon separated list of enabled inspections (defaults to all). false
-P:scapegoat:customInspectors: Colon separated list of custom inspections. false
-P:scapegoat:ignoredFiles: Colon separated list of regexes to match files to ignore. false
-P:scapegoat:verbose: Boolean flag that enables/disables verbose console messages. false
-P:scapegoat:consoleOutput: Boolean flag that enables/disables console report output. false
-P:scapegoat:reports: Colon separated list of reports to generate. Valid options are none, xml, html, scalastyle, markdown, ,gitlab-codequality, or all. false
-P:scapegoat:overrideLevels: Overrides the built in warning levels. Should be a colon separated list of name=level expressions. false
-P:scapegoat:sourcePrefix: Overrides source prefix if it differs from src/main/scala, for ex. app/ for Play applications. false
-P:scapegoat:minimalLevel: Provides minimal level of inspection displayed in reports and in the console. false

Reports

Here is sample output from the console during the build for a project with warnings/errors:

[warning] [scapegoat] Unused method parameter - org.ensime.util.ClassIterator.scala:46
[warning] [scapegoat] Unused method parameter - org.ensime.util.ClassIterator.scala:137
[warning] [scapegoat] Use of var - org.ensime.util.ClassIterator.scala:22
[warning] [scapegoat] Use of var - org.ensime.util.ClassIterator.scala:157
[   info] [scapegoat]: Inspecting compilation unit [FileUtil.scala]
[warning] [scapegoat] Empty if statement - org.ensime.util.FileUtil.scala:157
[warning] [scapegoat] Expression as statement - org.ensime.util.FileUtil.scala:180

And if you prefer a prettier report, here is a screen shot of the type of HTML report scapegoat generates:

screenshot

Configuration

For instructions on suppressing warnings by file, by inspection or by line see the sbt-scapegoat README.

To suppress warnings globally for the project, use disabledInspections or overrideLevels flags:

-P:scapegoat:disabledInspections:FinalModifierOnCaseClass
-P:scapegoat:overrideLevels:PreferSeqEmpty=ignore:AsInstanceOf=ignore

Inspections

There are currently 121 inspections for Scala 2, and 1 for Scala 3. An overview list is given, followed by a more detailed description of each inspection after the list (todo: finish rest of detailed descriptions)

Name Brief Description Default Level Scala 2 Scala 3
ArrayEquals Checks for comparison of arrays using == which will always return false Info Yes No
ArraysInFormat Checks for arrays passed to String.format Error Yes No
ArraysToString Checks for explicit toString calls on arrays Warning Yes No
AsInstanceOf Checks for use of asInstanceOf Warning Yes No
AvoidOperatorOverload Checks for mental symbolic method names Info Yes No
AvoidSizeEqualsZero Traversable.size can be slow for some data structure, prefer .isEmpty Warning Yes No
AvoidSizeNotEqualsZero Traversable.size can be slow for some data structure, prefer .nonEmpty Warning Yes No
AvoidToMinusOne Checks for loops that use x to n-1 instead of x until n Info Yes No
BigDecimalDoubleConstructor Checks for use of BigDecimal(double) which can be unsafe Warning Yes No
BigDecimalScaleWithoutRoundingMode setScale() on a BigDecimal without setting the rounding mode can throw an exception Warning Yes No
BooleanParameter Checks for functions that have a Boolean parameter Info Yes No
BoundedByFinalType Looks for types with upper bounds of a final type Warning Yes No
BrokenOddness Checks for a % 2 == 1 for oddness because this fails on negative numbers Warning Yes No
CatchException Checks for try blocks that catch Exception Warning Yes No
CatchExceptionImmediatelyRethrown Checks for try-catch blocks that immediately rethrow caught exceptions. Warning Yes No
CatchFatal Checks for try blocks that catch fatal exceptions: VirtualMachineError, ThreadDeath, InterruptedException, LinkageError, ControlThrowable Warning Yes No
CatchNpe Checks for try blocks that catch null pointer exceptions Error Yes No
CatchThrowable Checks for try blocks that catch Throwable Warning Yes No
ClassNames Ensures class names adhere to the style guidelines Info Yes No
CollectionIndexOnNonIndexedSeq Checks for indexing on a Seq which is not an IndexedSeq Warning Yes No
CollectionNamingConfusion Checks for variables that are confusingly named Info Yes No
CollectionNegativeIndex Checks for negative access on a sequence eg list.get(-1) Warning Yes No
CollectionPromotionToAny Checks for collection operations that promote the collection to Any Warning Yes No
ComparingFloatingPointTypes Checks for equality checks on floating point types Error Yes No
ComparingUnrelatedTypes Checks for equality comparisons that cannot succeed Error Yes No
ComparisonToEmptyList Checks for code like a == List() or a == Nil Info Yes No
ComparisonToEmptySet Checks for code like a == Set() or a == Set.empty Info Yes No
ComparisonWithSelf Checks for equality checks with itself Warning Yes No
ConstantIf Checks for code where the if condition compiles to a constant Warning Yes No
DivideByOne Checks for divide by one, which always returns the original value Warning Yes No
DoubleNegation Checks for code like !(!b) Info Yes No
DuplicateImport Checks for import statements that import the same selector Info Yes No
DuplicateMapKey Checks for duplicate key names in Map literals Warning Yes No
DuplicateSetValue Checks for duplicate values in set literals Warning Yes No
EitherGet Checks for use of .get on Left or Right Error Yes No
EmptyCaseClass Checks for case classes like case class Faceman() Info Yes No
EmptyFor Checks for empty for loops Warning Yes No
EmptyIfBlock Checks for empty if blocks Warning Yes No
EmptyInterpolatedString Looks for interpolated strings that have no arguments Error Yes No
EmptyMethod Looks for empty methods Warning Yes No
EmptySynchronizedBlock Looks for empty synchronized blocks Warning Yes No
EmptyTryBlock Looks for empty try blocks Warning Yes No
EmptyWhileBlock Looks for empty while loops Warning Yes No
ExistsSimplifiableToContains exists(x => x == b) replaceable with contains(b) Info Yes No
FilterDotHead .filter(x => ).head can be replaced with find(x => ) match { .. } Info Yes No
FilterDotHeadOption .filter(x =>).headOption can be replaced with find(x => ) Info Yes No
FilterDotIsEmpty .filter(x => ).isEmpty can be replaced with !exists(x => ) Info Yes No
FilterDotSize .filter(x => ).size can be replaced more concisely with with count(x => ) Info Yes No
FilterOptionAndGet .filter(_.isDefined).map(_.get) can be replaced with flatten Info Yes No
FinalModifierOnCaseClass Using Case classes without final modifier can lead to surprising breakage Info Yes No
FinalizerWithoutSuper Checks for overridden finalizers that do not call super Warning Yes No
FindAndNotEqualsNoneReplaceWithExists .find(x => ) != None can be replaced with exist(x => ) Info Yes No
FindDotIsDefined find(x => ).isDefined can be replaced with exist(x => ) Info Yes No
IllegalFormatString Looks for invalid format strings Error Yes No
ImpossibleOptionSizeCondition Checks for code like option.size > 2 which can never be true Error Yes No
IncorrectNumberOfArgsToFormat Checks for wrong number of arguments to String.format Error Yes No
IncorrectlyNamedExceptions Checks for exceptions that are not called *Exception and vice versa Error Yes No
InvalidRegex Checks for invalid regex literals Info Yes No
IsInstanceOf Checks for use of isInstanceOf Warning Yes No
JavaConversionsUse Checks for use of implicit Java conversions Warning Yes No
ListAppend Checks for List :+ which is O(n) Info Yes No
ListSize Checks for List.size which is O(n). Info Yes No
LonelySealedTrait Checks for sealed traits which have no implementation Error Yes No
LooksLikeInterpolatedString Finds strings that look like they should be interpolated but are not Warning Yes No
MapGetAndGetOrElse Map.get(key).getOrElse(value) can be replaced with Map.getOrElse(key, value) Error Yes No
MaxParameters Checks for methods that have over 10 parameters Info Yes No
MethodNames Warns on method names that don't adhere to the Scala style guidelines Info Yes No
MethodReturningAny Checks for defs that are defined or inferred to return Any Warning Yes No
ModOne Checks for x % 1 which will always return 0 Warning Yes No
NanComparison Checks for x == Double.NaN which will always fail Error Yes No
NegationIsEmpty !Traversable.isEmpty can be replaced with Traversable.nonEmpty Info Yes No
NegationNonEmpty !Traversable.nonEmpty can be replaced with Traversable.isEmpty Info Yes No
NoOpOverride Checks for code that overrides parent method but simply calls super Info Yes No
NullAssignment Checks for use of null in assignments Warning Yes No
NullParameter Checks for use of null in method invocation Warning Yes No
ObjectNames Ensures object names adhere to the Scala style guidelines Info Yes No
OptionGet Checks for Option.get Error Yes Yes
OptionSize Checks for Option.size Error Yes No
ParameterlessMethodReturnsUnit Checks for def foo : Unit Warning Yes No
PartialFunctionInsteadOfMatch Warns when you could use a partial function directly instead of a match block Info Yes No
PointlessTypeBounds Finds type bounds of the form [A <: Any] or [A >: Nothing] Warning Yes No
PreferMapEmpty Checks for Map() when could use Map.empty Info Yes No
PreferSeqEmpty Checks for Seq() when could use Seq.empty Info Yes No
PreferSetEmpty Checks for Set() when could use Set.empty Info Yes No
ProductWithSerializableInferred Checks for vals that have Product with Serializable as their inferred type Warning Yes No
PublicFinalizer Checks for overridden finalizes that are public Info Yes No
RedundantFinalModifierOnMethod Redundant final modifier on method that cannot be overridden Info Yes No
RedundantFinalModifierOnVar Redundant final modifier on var that cannot be overridden Info Yes No
RedundantFinalizer Checks for empty finalizers. Warning Yes No
RepeatedCaseBody Checks for case statements which have the same body Warning Yes No
RepeatedIfElseBody Checks for the main branch and the else branch of an if being the same Warning Yes No
ReverseFunc reverse followed by head, headOption, iterator, ormap can be replaced, respectively, with last, lastOption, reverseIterator, or reverseMap Info Yes No
ReverseTailReverse .reverse.tail.reverse can be replaced with init Info Yes No
ReverseTakeReverse .reverse.take(...).reverse can be replaced with takeRight Info Yes No
SimplifyBooleanExpression b == false can be simplified to !b Info Yes No
StoreBeforeReturn Checks for storing a value in a block, and immediately returning the value Info Yes No
StripMarginOnRegex Checks for .stripMargin on regex strings that contain a pipe Error Yes No
SubstringZero Checks for String.substring(0) Info Yes No
SuspiciousMatchOnClassObject Finds code where matching is taking place on class literals Warning Yes No
SwallowedException Finds catch blocks that don't handle caught exceptions Warning Yes No
SwapSortFilter sort.filter can be replaced with filter.sort for performance Info Yes No
TryGet Checks for use of Try.get Error Yes No
TypeShadowing Checks for shadowed type parameters in methods Warning Yes No
UnnecessaryConversion Checks for unnecessary toInt on instances of Int or toString on Strings, etc. Warning Yes No
UnnecessaryIf Checks for code like if (expr) true else false Info Yes No
UnnecessaryReturnUse Checks for use of return keyword in blocks Info Yes No
UnreachableCatch Checks for catch clauses that cannot be reached Warning Yes No
UnsafeContains Checks for List.contains(value) for invalid types Error Yes No
UnsafeStringContains Checks for String.contains(value) for invalid types Error Yes No
UnsafeTraversableMethods Check unsafe traversable method usages (head, tail, init, last, reduce, reduceLeft, reduceRight, max, maxBy, min, minBy) Error Yes No
UnusedMethodParameter Checks for unused method parameters Warning Yes No
UseCbrt Checks for use of math.pow for calculating math.cbrt Info Yes No
UseExpM1 Checks for use of math.exp(x) - 1 instead of math.expm1(x) Info Yes No
UseLog10 Checks for use of math.log(x)/math.log(10) instead of math.log10(x) Info Yes No
UseLog1P Checks for use of math.log(x + 1) instead of math.log1p(x) Info Yes No
UseSqrt Checks for use of math.pow for calculating math.sqrt Info Yes No
VarClosure Finds closures that reference var Warning Yes No
VarCouldBeVal Checks for vars that could be declared as vals Warning Yes No
VariableShadowing Checks for multiple uses of the variable name in nested scopes Warning Yes No
WhileTrue Checks for code that uses a while(true) or do { } while(true) block. Warning Yes No
ZeroNumerator Checks for dividing by 0 by a number, eg 0 / x which will always return 0 Warning Yes No
Arrays to string

Checks for explicit toString calls on arrays. Since toString on an array does not perform a deep toString, like say scala's List, this is usually a mistake.

CollectionIndexOnNonIndexedSeq

Checks for calls of .apply(idx) on a Seq where the index is not a literal and the Seq is not an IndexedSeq.

Rationale If code which expects O(1) positional access to a Seq is given a non-IndexedSeq (such as a List, where indexing is O(n)) then this may cause poor performance.

ComparingUnrelatedTypes

Checks for equality comparisons that cannot succeed because the types are unrelated. Eg "string" == BigDecimal(1.0). The scala compiler has a less strict version of this inspection.

ConstantIf

Checks for if statements where the condition is always true or false. Not only checks for the boolean literals, but also any expression that the compiler is able to turn into a constant value. Eg, if (0 < 1) then else that

IllegalFormatString

Checks for a format string that is not invalid, such as invalid conversions, invalid flags, etc. Eg, "% s", "%qs", %.-4f"

IncorrectNumberOfArgsToFormat

Checks for an incorrect number of arguments to String.format. Eg, "%s %s %f".format("need", "three") flags an error because the format string specifies 3 parameters but the call only provides 2.

InvalidRegex

Checks for invalid regex literals that would fail at compile time. Either dangling metacharacters, or unclosed escape characters, etc that kind of thing.

List size

Checks for .size on an instance of List. Eg, val a = List(1,2,3); a.size

Rationale: List.size is O(n) so for performance reasons if .size is needed on a list that could be large, consider using an alternative with O(1), eg Array, Vector or ListBuffer.

Redundant finalizer

Checks for empty finalizers. This is redundant code and should be removed. Eg, override def finalize : Unit = { }

PreferSetEmpty

Indicates where code using Set() could be replaced with Set.empty. Set() instantiates a new instance each time it is invoked, whereas Set.empty returns a pre-instantiated instance.

UnnecessaryReturnUse

Checks for use of return in a function or method. Since the final expression of a block is always the return value, using return is unnecessary. Eg, def foo = { println("hello"); return 12; }

UnreachableCatch

Checks for catch clauses that cannot be reached. This means the exception is dead and if you want that exception to take precedence you should move up further up the case list.

UnsafeContains

Checks for List.contains(value) for invalid types. The method for contains accepts any types. This inspection finds situations when you have a list of type A and you are checking for contains on type B which cannot hold.

While true

Checks for code that uses a while(true) or do { } while(true) block.

Rationale: This type of code is usually not meant for production as it will not return normally. If you need to loop until interrupted, then consider using a flag.

Suppressing Warnings by Method or Class

You can suppress a specific warning by method or by class using the java.lang.SuppressWarnings annotation.

Use the simple name of the inspection to be ignored as the argument, or use "all" to suppress all scapegoat warnings in the specified scope.

Some examples:

@SuppressWarnings(Array("all"))
class Test {
  def hello: Unit = {
    val s: Any = "sammy"
    println(s.asInstanceOf[String])
  }
}

class Test2 {
  @SuppressWarnings(Array("AsInstanceOf"))
  def hello: Unit = {
    val s: Any = "sammy"
    println(s.asInstanceOf[String])
  }
}

Other static analysis tools:

About

Scala compiler plugin for static code analysis

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Scala 99.9%
  • Java 0.1%