Skip to content

Analyzer enhancements #696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
22b6e0f
add ImplicitValueClasses rule to enforce extending AnyVal for implici…
halotukozak Apr 15, 2025
e3c37cc
add FinalValueClasses rule to enforce marking value classes as final
halotukozak Apr 15, 2025
b189364
add ImplicitParamDefaults rule to prevent default values for implicit…
halotukozak Apr 15, 2025
7edce9a
ImplicitValueClasses rule: divide test into several ones, add Univers…
halotukozak Apr 17, 2025
9964dd4
do not run same tests twice
halotukozak Apr 17, 2025
746d402
refactor ImplicitValueClasses rule: move message construction inside …
halotukozak Apr 17, 2025
1a234f9
add CatchThrowable rule to discourage catching Throwable directly and…
halotukozak Apr 17, 2025
fbafdfb
add ImplicitFunctionParams rule to prevent implicit parameters from b…
halotukozak Apr 17, 2025
0f19b67
remove support for SAM types in ImplicitFunctionParams rule and enhan…
halotukozak Apr 17, 2025
441f2f6
enhance CatchThrowable rule to allow catching NonFatal exceptions and…
halotukozak Apr 17, 2025
04ff725
support NonFatal alias also
halotukozak Apr 17, 2025
1856639
add FinalCaseClasses rule to enforce final modifier on case classes a…
halotukozak Apr 18, 2025
563f6b4
enhance ImplicitValueClasses rule to prevent implicit classes from be…
halotukozak Apr 18, 2025
f37a935
enhance FinalCaseClasses rule to handle case classes defined inside t…
halotukozak Apr 18, 2025
d861b8c
enhance CatchThrowable rule to reject catching Throwable with pattern…
halotukozak Apr 25, 2025
4975fda
enhance CatchThrowable rule to improve pattern matching checks and ad…
halotukozak Apr 25, 2025
b72090f
enhance CatchThrowable rule to allow catching Throwable with custom e…
halotukozak Apr 25, 2025
a429ef1
enhance FinalCaseClasses rule to allow sealed case classes and add co…
halotukozak Apr 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin =>
new BadSingletonComponent(global),
new ConstantDeclarations(global),
new BasePackage(global),
new ImplicitValueClasses(global),
new FinalValueClasses(global),
new FinalCaseClasses(global),
new ImplicitParamDefaults(global),
new CatchThrowable(global),
new ImplicitFunctionParams(global),
)

private lazy val rulesByName = rules.map(r => (r.name, r)).toMap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ abstract class AnalyzerRule(val global: Global, val name: String, defaultLevel:
pos: Position,
message: String,
category: WarningCategory = WarningCategory.Lint,
site: Symbol = NoSymbol
site: Symbol = NoSymbol,
level: Level = this.level
): Unit =
level match {
case Level.Off =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class CatchThrowable(g: Global) extends AnalyzerRule(g, "catchThrowable", Level.Warn) {

import global.*

private lazy val throwableTpe = typeOf[Throwable]

private def isCustomExtractor(tree: Tree): Boolean = tree match {
case UnApply(Apply(Select(_, TermName("unapply")), _), _) => true
case _ => false
}

private def checkTree(pat: Tree): Unit = if (pat.tpe != null && pat.tpe =:= throwableTpe && !isCustomExtractor(pat)) {
report(pat.pos, "Catching Throwable is discouraged, catch specific exceptions instead")
}

def analyze(unit: CompilationUnit): Unit =
unit.body.foreach {
case t: Try =>
t.catches.foreach {
case CaseDef(Alternative(trees), _, _) => trees.foreach(checkTree)
case CaseDef(Bind(_, Alternative(trees)), _, _) => trees.foreach(checkTree)
case CaseDef(pat, _, _) => checkTree(pat)
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.avsystem.commons
package analyzer

import com.avsystem.commons.analyzer.Level.Info

import scala.tools.nsc.Global

class FinalCaseClasses(g: Global) extends AnalyzerRule(g, "finalCaseClasses", Level.Warn) {

import global.*

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case cd: ClassDef if !cd.mods.hasFlag(Flag.FINAL | Flag.SEALED) && cd.mods.hasFlag(Flag.CASE) =>
// Skip case classes defined inside traits (SI-4440)
val isInner = cd.symbol.isStatic

if (isInner) {
report(cd.pos, "Case classes should be marked as final")
} else {
report(cd.pos, "Case classes should be marked as final. Due to the SI-4440 bug, it cannot be done here. Consider moving the case class to the companion object", level = Info)
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class FinalValueClasses(g: Global) extends AnalyzerRule(g, "finalValueClasses", Level.Warn) {

import global.*

private lazy val anyValTpe = typeOf[AnyVal]

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case cd: ClassDef if !cd.mods.hasFlag(Flag.FINAL) =>
val tpe = cd.symbol.typeSignature

if (tpe.baseClasses.contains(anyValTpe.typeSymbol) ) {
report(cd.pos, "Value classes should be marked as final")
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class ImplicitFunctionParams(g: Global) extends AnalyzerRule(g, "implicitFunctionParams", Level.Warn) {

import global.*

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case dd: DefDef =>
dd.vparamss.foreach { paramList =>
if (paramList.nonEmpty && paramList.head.mods.hasFlag(Flag.IMPLICIT)) {
paramList.foreach { param =>
val paramTpe = param.tpt.tpe
if (paramTpe != null && (definitions.isFunctionType(paramTpe) || definitions.isPartialFunctionType(paramTpe))) {
report(param.pos, "Implicit parameters should not have any function type")
}
}
}
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class ImplicitParamDefaults(g: Global) extends AnalyzerRule(g, "implicitParamDefaults", Level.Warn) {

import global.*

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case dd: DefDef =>
dd.vparamss.foreach { paramList =>
if (paramList.nonEmpty && paramList.head.mods.hasFlag(Flag.IMPLICIT)) {
paramList.foreach { param =>
if (param.rhs != EmptyTree) {
report(param.pos, "Do not declare default values for implicit parameters")
}
}
}
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class ImplicitValueClasses(g: Global) extends AnalyzerRule(g, "implicitValueClasses", Level.Warn) {

import global.*
import definitions.*

private lazy val defaultClasses = Set[Symbol](AnyClass, AnyValClass, ObjectClass)

private lazy val reportOnNestedClasses = argument match {
case null => false
case a => a.toBoolean
}

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case cd: ClassDef if cd.mods.hasFlag(Flag.IMPLICIT) =>
val tpe = cd.symbol.typeSignature
val primaryCtor = tpe.member(termNames.CONSTRUCTOR).alternatives.find(_.asMethod.isPrimaryConstructor)
val paramLists = primaryCtor.map(_.asMethod.paramLists)
val inheritsAnyVal = tpe.baseClasses contains AnyValClass

def inheritsOtherClass = tpe.baseClasses.exists { cls =>
def isDefault = defaultClasses contains cls
def isUniversalTrait = cls.isTrait && cls.superClass == AnyClass

cls != cd.symbol && !isDefault && !isUniversalTrait
}
def hasExactlyOneParam = paramLists.exists(lists => lists.size == 1 && lists.head.size == 1)

def paramIsValueClass = paramLists.exists { lists =>
/* lists.nonEmpty && lists.head.nonEmpty && */
lists.head.head.typeSignature.typeSymbol.isDerivedValueClass
}

if (!inheritsAnyVal && !inheritsOtherClass && hasExactlyOneParam && !paramIsValueClass) {
val isNestedClass =
//implicit classes are always nested classes, so we want to check if the outer class's an object
/*cd.symbol.isNestedClass &&*/ !cd.symbol.isStatic

val message = "Implicit classes should always extend AnyVal to become value classes" +
(if (isNestedClass) ". Nested classes should be extracted to top-level objects" else "")

if (reportOnNestedClasses || !isNestedClass)
report(cd.pos, message)
else
report(cd.pos, message, level = Level.Info)
}
case _ =>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.avsystem.commons
package analyzer

import org.scalatest.funsuite.AnyFunSuite

class CatchThrowableTest extends AnyFunSuite with AnalyzerTest {
test("catching Throwable should be rejected") {
assertErrors(1,
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case t: Throwable => println(t)
| }
| }
|}
""".stripMargin)
}

test("catching specific exceptions should be allowed") {
assertNoErrors(
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case e: Exception => println(e)
| case e: RuntimeException => println(e)
| case e: IllegalArgumentException => println(e)
| }
| }
|}
""".stripMargin)
}

test("catching Throwable with other exceptions should be rejected") {
assertErrors(1,
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case e: IllegalArgumentException => println(e)
| case t: Throwable => println(t)
| }
| }
|}
""".stripMargin)
}

test("catching Throwable in nested catch block should be rejected") {
assertErrors(1,
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try println("test")
| catch {
| case e: Exception => try println("test")
| catch {
| case e: Throwable => println(e)
| }
| }
| }
|}
""".stripMargin)
}

test("catching Throwable using custom extractor should be allowed") {
assertNoErrors(
//language=Scala
"""
|object Test extends com.avsystem.commons.CommonAliases {
| object custom {
| def unapply(t: Throwable): Option[IllegalArgumentException] = t match {
| case e: IllegalArgumentException => Some(e)
| case _ => None
| }
| }
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case custom(t) => println(t)
| case NonFatal(t) => println(t)
| case scala.util.control.NonFatal(t) => println(t)
| }
| }
|}
""".stripMargin)
}

test("catching non-Throwable with pattern match should be allowed") {
assertNoErrors(
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case _: IndexOutOfBoundsException | _: NullPointerException => println("OK!")
| }
| try {
| println("test")
| } catch {
| case e@(_: IndexOutOfBoundsException | _: NullPointerException) => println("OK!")
| }
| }
|}
""".stripMargin)
}

test("catching Throwable with pattern match should be rejected") {
assertErrors(1,
//language=Scala
"""
|object Test {
| def test(): Unit = {
| try {
| println("test")
| } catch {
| case _: IndexOutOfBoundsException | _: Throwable => println("Not OK!")
| }
| }
|}
""".stripMargin)
}
}
Loading