Skip to content

Commit f9e48dd

Browse files
committed
Merge remote-tracking branch 'origin/master' into 500-handle-thrown-exception-outside-the-monadic-context-of-task
# Conflicts: # docs/Analyzer.md
2 parents 6222088 + 3f29426 commit f9e48dd

16 files changed

+860
-18
lines changed

analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin =>
6060
new BadSingletonComponent(global),
6161
new ConstantDeclarations(global),
6262
new BasePackage(global),
63+
new ImplicitValueClasses(global),
64+
new FinalValueClasses(global),
65+
new FinalCaseClasses(global),
66+
new ImplicitParamDefaults(global),
67+
new CatchThrowable(global),
68+
new ImplicitFunctionParams(global),
6369
)
6470

6571
private lazy val rulesByName = rules.map(r => (r.name, r)).toMap

analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ abstract class AnalyzerRule(val global: Global, val name: String, defaultLevel:
3232
pos: Position,
3333
message: String,
3434
category: WarningCategory = WarningCategory.Lint,
35-
site: Symbol = NoSymbol
35+
site: Symbol = NoSymbol,
36+
level: Level = this.level
3637
): Unit =
3738
level match {
3839
case Level.Off =>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import scala.tools.nsc.Global
5+
6+
class CatchThrowable(g: Global) extends AnalyzerRule(g, "catchThrowable", Level.Warn) {
7+
8+
import global.*
9+
10+
private lazy val throwableTpe = typeOf[Throwable]
11+
12+
private def isCustomExtractor(tree: Tree): Boolean = tree match {
13+
case UnApply(Apply(Select(_, TermName("unapply")), _), _) => true
14+
case _ => false
15+
}
16+
17+
private def checkTree(pat: Tree): Unit = if (pat.tpe != null && pat.tpe =:= throwableTpe && !isCustomExtractor(pat)) {
18+
report(pat.pos, "Catching Throwable is discouraged, catch specific exceptions instead")
19+
}
20+
21+
def analyze(unit: CompilationUnit): Unit =
22+
unit.body.foreach {
23+
case t: Try =>
24+
t.catches.foreach {
25+
case CaseDef(Alternative(trees), _, _) => trees.foreach(checkTree)
26+
case CaseDef(Bind(_, Alternative(trees)), _, _) => trees.foreach(checkTree)
27+
case CaseDef(pat, _, _) => checkTree(pat)
28+
}
29+
case _ =>
30+
}
31+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import com.avsystem.commons.analyzer.Level.Info
5+
6+
import scala.tools.nsc.Global
7+
8+
class FinalCaseClasses(g: Global) extends AnalyzerRule(g, "finalCaseClasses", Level.Warn) {
9+
10+
import global.*
11+
12+
def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
13+
case cd: ClassDef if !cd.mods.hasFlag(Flag.FINAL | Flag.SEALED) && cd.mods.hasFlag(Flag.CASE) =>
14+
// Skip case classes defined inside traits (SI-4440)
15+
val isInner = cd.symbol.isStatic
16+
17+
if (isInner) {
18+
report(cd.pos, "Case classes should be marked as final")
19+
} else {
20+
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)
21+
}
22+
case _ =>
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import scala.tools.nsc.Global
5+
6+
class FinalValueClasses(g: Global) extends AnalyzerRule(g, "finalValueClasses", Level.Warn) {
7+
8+
import global.*
9+
10+
private lazy val anyValTpe = typeOf[AnyVal]
11+
12+
def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
13+
case cd: ClassDef if !cd.mods.hasFlag(Flag.FINAL) =>
14+
val tpe = cd.symbol.typeSignature
15+
16+
if (tpe.baseClasses.contains(anyValTpe.typeSymbol) ) {
17+
report(cd.pos, "Value classes should be marked as final")
18+
}
19+
case _ =>
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import scala.tools.nsc.Global
5+
6+
class ImplicitFunctionParams(g: Global) extends AnalyzerRule(g, "implicitFunctionParams", Level.Warn) {
7+
8+
import global.*
9+
10+
def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
11+
case dd: DefDef =>
12+
dd.vparamss.foreach { paramList =>
13+
if (paramList.nonEmpty && paramList.head.mods.hasFlag(Flag.IMPLICIT)) {
14+
paramList.foreach { param =>
15+
val paramTpe = param.tpt.tpe
16+
if (paramTpe != null && (definitions.isFunctionType(paramTpe) || definitions.isPartialFunctionType(paramTpe))) {
17+
report(param.pos, "Implicit parameters should not have any function type")
18+
}
19+
}
20+
}
21+
}
22+
case _ =>
23+
}
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import scala.tools.nsc.Global
5+
6+
class ImplicitParamDefaults(g: Global) extends AnalyzerRule(g, "implicitParamDefaults", Level.Warn) {
7+
8+
import global.*
9+
10+
def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
11+
case dd: DefDef =>
12+
dd.vparamss.foreach { paramList =>
13+
if (paramList.nonEmpty && paramList.head.mods.hasFlag(Flag.IMPLICIT)) {
14+
paramList.foreach { param =>
15+
if (param.rhs != EmptyTree) {
16+
report(param.pos, "Implicit parameters should not have default values")
17+
}
18+
}
19+
}
20+
}
21+
case _ =>
22+
}
23+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import scala.tools.nsc.Global
5+
6+
class ImplicitValueClasses(g: Global) extends AnalyzerRule(g, "implicitValueClasses", Level.Warn) {
7+
8+
import global.*
9+
import definitions.*
10+
11+
private lazy val defaultClasses = Set[Symbol](AnyClass, AnyValClass, ObjectClass)
12+
13+
private lazy val reportOnNestedClasses = argument match {
14+
case "all" => true
15+
case "top-level-only" | null => false
16+
case _ => throw new IllegalArgumentException(s"Unknown ImplicitValueClasses option: $argument")
17+
}
18+
19+
def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
20+
case cd: ClassDef if cd.mods.hasFlag(Flag.IMPLICIT) =>
21+
val tpe = cd.symbol.typeSignature
22+
val primaryCtor = tpe.member(termNames.CONSTRUCTOR).alternatives.find(_.asMethod.isPrimaryConstructor)
23+
val paramLists = primaryCtor.map(_.asMethod.paramLists)
24+
val inheritsAnyVal = tpe.baseClasses contains AnyValClass
25+
26+
def inheritsOtherClass = tpe.baseClasses.exists { cls =>
27+
def isDefault = defaultClasses contains cls
28+
29+
def isUniversalTrait = cls.isTrait && cls.superClass == AnyClass
30+
31+
cls != cd.symbol && !isDefault && !isUniversalTrait
32+
}
33+
34+
def hasExactlyOneParam = paramLists.exists(lists => lists.size == 1 && lists.head.size == 1)
35+
36+
def paramIsValueClass = paramLists.exists { lists =>
37+
/* lists.nonEmpty && lists.head.nonEmpty && */
38+
lists.head.head.typeSignature.typeSymbol.isDerivedValueClass
39+
}
40+
41+
if (!inheritsAnyVal && !inheritsOtherClass && hasExactlyOneParam && !paramIsValueClass) {
42+
val isNestedClass =
43+
//implicit classes are always nested classes, so we want to check if the outer class's an object
44+
/*cd.symbol.isNestedClass &&*/ !cd.symbol.isStatic
45+
46+
val message = "Implicit classes should always extend AnyVal to become value classes" +
47+
(if (isNestedClass) ". Nested classes should be extracted to top-level objects" else "")
48+
49+
if (reportOnNestedClasses || !isNestedClass)
50+
report(cd.pos, message)
51+
else
52+
report(cd.pos, message, level = Level.Info)
53+
}
54+
case _ =>
55+
}
56+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.avsystem.commons
2+
package analyzer
3+
4+
import org.scalatest.funsuite.AnyFunSuite
5+
6+
final class CatchThrowableTest extends AnyFunSuite with AnalyzerTest {
7+
test("catching Throwable should be rejected") {
8+
assertErrors(1,
9+
scala"""
10+
|def test(): Unit =
11+
| try {
12+
| println("test")
13+
| } catch {
14+
| case t: Throwable => println(t)
15+
| }
16+
|""".stripMargin)
17+
}
18+
19+
test("catching specific exceptions should be allowed") {
20+
assertNoErrors(
21+
scala"""
22+
|def test(): Unit =
23+
| try {
24+
| println("test")
25+
| } catch {
26+
| case e: Exception => println(e)
27+
| case e: RuntimeException => println(e)
28+
| case e: IllegalArgumentException => println(e)
29+
| }
30+
|""".stripMargin)
31+
}
32+
33+
test("catching Throwable with other exceptions should be rejected") {
34+
assertErrors(1,
35+
scala"""
36+
|def test(): Unit = {
37+
| try {
38+
| println("test")
39+
| } catch {
40+
| case e: IllegalArgumentException => println(e)
41+
| case t: Throwable => println(t)
42+
| }
43+
|}
44+
|""".stripMargin)
45+
}
46+
47+
test("catching Throwable in nested catch block should be rejected") {
48+
assertErrors(1,
49+
scala"""
50+
|def test(): Unit = {
51+
| try println("test")
52+
| catch {
53+
| case e: Exception => try println("test")
54+
| catch {
55+
| case e: Throwable => println(e)
56+
| }
57+
| }
58+
|}
59+
|""".stripMargin)
60+
}
61+
62+
test("catching Throwable using custom extractor should be allowed") {
63+
assertNoErrors(
64+
scala"""
65+
|import com.avsystem.commons.CommonAliases._
66+
|
67+
|object custom {
68+
| def unapply(t: Throwable): Option[IllegalArgumentException] = t match {
69+
| case e: IllegalArgumentException => Some(e)
70+
| case _ => None
71+
| }
72+
|}
73+
|def test(): Unit = {
74+
| try {
75+
| println("test")
76+
| } catch {
77+
| case custom(t) => println(t)
78+
| case NonFatal(t) => println(t)
79+
| case scala.util.control.NonFatal(t) => println(t)
80+
| }
81+
|}
82+
|""".stripMargin)
83+
}
84+
85+
test("catching non-Throwable with pattern match should be allowed") {
86+
assertNoErrors(
87+
scala"""
88+
|def test(): Unit = {
89+
| try {
90+
| println("test")
91+
| } catch {
92+
| case _: IndexOutOfBoundsException | _: NullPointerException => println("OK!")
93+
| }
94+
| try {
95+
| println("test")
96+
| } catch {
97+
| case e@(_: IndexOutOfBoundsException | _: NullPointerException) => println("OK!")
98+
| }
99+
|}
100+
|""".stripMargin)
101+
}
102+
103+
test("catching Throwable with pattern match should be rejected") {
104+
assertErrors(1,
105+
scala"""
106+
|def test(): Unit = {
107+
| try {
108+
| println("test")
109+
| } catch {
110+
| case _: IndexOutOfBoundsException | _: Throwable => println("Not OK!")
111+
| }
112+
|}
113+
|""".stripMargin)
114+
}
115+
}

0 commit comments

Comments
 (0)