Skip to content

Analyzer rule for Scala constant declarations #423

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

Merged
merged 5 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -56,7 +56,8 @@ final class AnalyzerPlugin(val global: Global) extends Plugin { plugin =>
new Any2StringAdd(global),
new ThrowableObjects(global),
new DiscardedMonixTask(global),
new BadSingletonComponent(global)
new BadSingletonComponent(global),
new ConstantDeclarations(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 @@ -2,8 +2,8 @@ package com.avsystem.commons
package analyzer

import java.io.{PrintWriter, StringWriter}

import scala.tools.nsc.Global
import scala.tools.nsc.Reporting.WarningCategory
import scala.util.control.NonFatal

abstract class AnalyzerRule(val global: Global, val name: String, defaultLevel: Level = Level.Warn) {
Expand All @@ -28,11 +28,16 @@ abstract class AnalyzerRule(val global: Global, val name: String, defaultLevel:

private def adjustMsg(msg: String): String = s"[AVS] $msg"

protected def report(pos: Position, message: String): Unit =
protected final def report(
pos: Position,
message: String,
category: WarningCategory = WarningCategory.Lint,
site: Symbol = NoSymbol
): Unit =
level match {
case Level.Off =>
case Level.Info => reporter.echo(pos, adjustMsg(message))
case Level.Warn => reporter.warning(pos, adjustMsg(message))
case Level.Warn => currentRun.reporting.warning(pos, adjustMsg(message), category, site)
case Level.Error => reporter.error(pos, adjustMsg(message))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.avsystem.commons
package analyzer

import scala.tools.nsc.Global

class ConstantDeclarations(g: Global) extends AnalyzerRule(g, "constantDeclarations", Level.Off) {

import global._

def analyze(unit: CompilationUnit): Unit = unit.body.foreach {
case t@ValDef(_, name, tpt, rhs)
if t.symbol.hasGetter && t.symbol.owner.isEffectivelyFinal =>

val getter = t.symbol.getterIn(t.symbol.owner)
if (getter.isPublic && getter.isStable && getter.overrides.isEmpty) {
val constantValue = rhs.tpe match {
case ConstantType(_) => true
case _ => false
}

def doReport(msg: String): Unit =
report(t.pos, msg, site = t.symbol)

val firstChar = name.toString.charAt(0)
if (constantValue && (firstChar.isLower || !getter.isFinal)) {
doReport("a literal-valued constant should be declared as a `final val` with an UpperCamelCase name")
}
if (!constantValue && firstChar.isUpper && !getter.isFinal) {
doReport("a constant with UpperCamelCase name should be declared as a `final val`")
}
if (getter.isFinal && constantValue && !(tpt.tpe =:= rhs.tpe)) {
doReport("a constant with a literal value should not have an explicit type annotation")
}
}
case _ =>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class CheckMacroPrivateTest extends AnyFunSuite with AnalyzerTest {
|object test {
| @macroPrivate def macroPrivateMethod = { println("whatever"); 5 }
| @macroPrivate object macroPrivateObject {
| val x = 42
| final val X = 42
| }
|}
""".stripMargin
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.avsystem.commons
package analyzer

import org.scalatest.funsuite.AnyFunSuite

class ConstantDeclarationsTest extends AnyFunSuite with AnalyzerTest {
test("literal-valued constants should be non-lazy final vals with UpperCamelCase and no type annotation") {
assertErrors(4,
"""
|object Whatever {
| // bad
| val a = 10
| val B = 10
| final val c = 10
| final val D: Int = 10
|
| // good
| final val E = 10
|}
""".stripMargin)
}

test("effectively final, non-literal UpperCamelCase vals should be final") {
assertErrors(1,
"""
|object Whatever {
| // bad
| val A = "foo".trim
|
| // good
| final val B = "foo".trim
| val c = "foo".trim
|}
""".stripMargin)
}

test("no constant checking in traits or non-final classes") {
assertNoErrors(
"""
|trait Whatever {
| val a = 10
| val B = 10
| final val c = 10
| final val D: Int = 10
| val A = "foo".trim
|}
|
|class Stuff {
| val a = 10
| val B = 10
| final val c = 10
| final val D: Int = 10
| val A = "foo".trim
|}
""".stripMargin)
}

test("no constant checking for overrides") {
assertNoErrors(
"""
|trait Whatever {
| def a: Int
|}
|
|object Stuff extends Whatever {
| val a: Int = 42
|}
""".stripMargin)
}

test("no constant checking for privates") {
assertNoErrors(
"""
|object Whatever {
| private val a = 10
| private val B = 10
| private final val c = 10
| private final val D: Int = 10
| private val A = "foo".trim
|}
""".stripMargin)
}
}
25 changes: 15 additions & 10 deletions docs/Analyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,31 @@ and [silencer](https://github.com/ghik/silencer) plugin for warning suppression.

Here's a list of currently supported rules:

| Name | Default level | Description |
| --- | --- | --- |
| `importJavaUtil` | warning | Rejects direct imports of `java.util` package. |
| Name | Default level | Description |
|----------------------------| --- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `importJavaUtil` | warning | Rejects direct imports of `java.util` package. |
| `valueEnumExhaustiveMatch` | warning | Enables (limited) exhaustive pattern match checking for [`ValueEnum`s](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/misc/ValueEnum.scala). |
| `any2stringadd` | off | Disables `any2stringadd` (concatenating arbitrary values with strings using `+`). |
| `bincompat` | warning | Enables [`@bincompat`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/bincompat.scala) checking |
| `showAst` | error | Handles [`@showAst`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/showAst.scala). |
| `findUsages` | warning | Issues a message for every occurrence of any of the symbols listed in the argument of this rule |
| `varargsAtLeast` | warning | Enables [`@atLeast`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/atLeast.scala) checking |
| `macroPrivate` | warning | Enables [`@macroPrivate`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/macroPrivate.scala) checking |
| `explicitGenerics` | warning | Enables [`@explicitGenerics`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/explicitGenerics.scala) checking |
| `any2stringadd` | off | Disables `any2stringadd` (concatenating arbitrary values with strings using `+`). |
| `bincompat` | warning | Enables [`@bincompat`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/bincompat.scala) checking |
| `showAst` | error | Handles [`@showAst`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/showAst.scala). |
| `findUsages` | warning | Issues a message for every occurrence of any of the symbols listed in the argument of this rule |
| `varargsAtLeast` | warning | Enables [`@atLeast`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/atLeast.scala) checking |
| `macroPrivate` | warning | Enables [`@macroPrivate`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/macroPrivate.scala) checking |
| `explicitGenerics` | warning | Enables [`@explicitGenerics`](https://github.com/AVSystem/scala-commons/blob/master/commons-core/src/main/scala/com/avsystem/commons/annotation/explicitGenerics.scala) checking |
| `discardedMonixTask` | warning | Makes sure that expressions evaluating to Monix `Task`s are not accidentally discarded by conversion to `Unit` |
| `throwableObjects` | warning | Makes sure that objects are never used as `Throwable`s (unless they have stack traces disabled) |
| `constantDeclarations` | warning | Checks if constants are always declared as `final val`s with `UpperCamelCase` and no explicit type annotation for literal values |

Rules may be enabled and disabled in `build.sbt` with Scala compiler options:

```scala
scalacOptions += "-P:AVSystemAnalyzer:<level><ruleName>,<level><ruleName>,..."
```

`<name>` must be one of the rule names listed above or `_` to apply to all rules.

`<level>` may be one of:

* `-` to disable rule
* `*` for "info" level
* _empty_ for "warning" level
Expand Down