From bec4e5a4d4a84c82366ea5124caa0a6a957db7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 21 Aug 2025 19:13:28 +0200 Subject: [PATCH 1/6] Add explicit generics rules and tests for constructors in Analyzer Plugin --- .../analyzer/ExplicitGenericsTest.scala | 18 ++++++++++++++++++ .../avsystem/commons/analyzer/TestUtils.scala | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala index 575bbce51..87e6cb3bb 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala @@ -41,4 +41,22 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { |val x = TestUtils.genericMacro[Int](123) |""".stripMargin) } + + test("inferred in constructor should be rejected") { + assertErrors(1, + scala""" + |import com.avsystem.commons.analyzer.TestUtils + | + |val x = new TestUtils.GenericClass() + |""".stripMargin) + } + + test("explicit in constructor should not be rejected") { + assertNoErrors( + scala""" + |import com.avsystem.commons.analyzer.TestUtils + | + |val x = new TestUtils.GenericClass[Int]() + |""".stripMargin) + } } diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala index 9a4e00c70..0fd6250d4 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala @@ -28,4 +28,9 @@ object TestUtils { def genericMethod[T](arg: T): T = arg @explicitGenerics def genericMacro[T](arg: T): T = macro genericMacroImpl[T] + + @explicitGenerics + class GenericClass[T] + + case class GenericCaseClass[T](arg: T) } From bfcaf8a15e7d9f8a5281d106c1ac4c0882221a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 21 Aug 2025 19:27:02 +0200 Subject: [PATCH 2/6] Refactor explicit generics rule to extend checks to constructors --- .../commons/analyzer/ExplicitGenerics.scala | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala index 585f58ce5..f4326b57e 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala @@ -9,7 +9,11 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { lazy val explicitGenericsAnnotTpe = classType("com.avsystem.commons.annotation.explicitGenerics") - def analyze(unit: CompilationUnit) = if (explicitGenericsAnnotTpe != NoType) { + + private def fail(pos: Position, symbol: Symbol): Unit = + report(pos, s"$symbol requires that its type arguments are explicit (not inferred)") + + def analyze(unit: CompilationUnit): Unit = if (explicitGenericsAnnotTpe != NoType) { def requiresExplicitGenerics(sym: Symbol): Boolean = sym != NoSymbol && (sym :: sym.overrides).flatMap(_.annotations).exists(_.tree.tpe <:< explicitGenericsAnnotTpe) @@ -22,7 +26,18 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { case _ => false } if (inferredTypeParams) { - report(t.pos, s"${pre.symbol} requires that its type arguments are explicit (not inferred)") + fail(t.pos, pre.symbol) + } + case n@New(tpt) if requiresExplicitGenerics(tpt.tpe.typeSymbol) => + val explicitTypeArgsProvided = tpt match { + case tt: TypeTree => tt.original match { + case AppliedTypeTree(_, args) if args.nonEmpty => true + case _ => false + } + case _ => false + } + if (!explicitTypeArgsProvided) { + fail(n.pos, tpt.tpe.typeSymbol) } case _ => } From e218978e8fd90a848ec7c99dfb37f607585a8728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 18 Sep 2025 19:22:11 +0200 Subject: [PATCH 3/6] add tests for explicit generics on case classes --- .../analyzer/ExplicitGenericsTest.scala | 27 ++++++++++++++++++- .../avsystem/commons/analyzer/TestUtils.scala | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala index 87e6cb3bb..9c7181e73 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala @@ -43,11 +43,22 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { } test("inferred in constructor should be rejected") { - assertErrors(1, + assertErrors(2, scala""" |import com.avsystem.commons.analyzer.TestUtils | |val x = new TestUtils.GenericClass() + |val y = new TestUtils.GenericCaseClass(123) + |""".stripMargin) + } + + + test("inferred in apply when constructor marked should be rejected") { + assertErrors(1, + scala""" + |import com.avsystem.commons.analyzer.TestUtils + | + |val x = TestUtils.GenericCaseClass(123) |""".stripMargin) } @@ -59,4 +70,18 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { |val x = new TestUtils.GenericClass[Int]() |""".stripMargin) } + + test("not marked should not be rejected") { + assertNoErrors( + scala""" + |def method[T](e: T) = e + |class NotMarkedGenericClass[T] + |final case class NotMarkedGenericCaseClass[T](arg: T) + | + |val w = method(123) + |val x = new NotMarkedGenericClass() + |val y = NotMarkedGenericCaseClass(123) + |val z = new NotMarkedGenericClass() + |""".stripMargin) + } } diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala index 0fd6250d4..dfb82d724 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/TestUtils.scala @@ -32,5 +32,6 @@ object TestUtils { @explicitGenerics class GenericClass[T] + @explicitGenerics case class GenericCaseClass[T](arg: T) } From 297baac4b5cba3f4b47cc89d33f50b1982560630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Thu, 18 Sep 2025 19:26:12 +0200 Subject: [PATCH 4/6] enhance explicit generics rule to handle companion `apply` methods --- .../commons/analyzer/ExplicitGenerics.scala | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala index f4326b57e..207481dd5 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala @@ -5,7 +5,7 @@ import scala.tools.nsc.Global class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { - import global._ + import global.* lazy val explicitGenericsAnnotTpe = classType("com.avsystem.commons.annotation.explicitGenerics") @@ -17,16 +17,29 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { def requiresExplicitGenerics(sym: Symbol): Boolean = sym != NoSymbol && (sym :: sym.overrides).flatMap(_.annotations).exists(_.tree.tpe <:< explicitGenericsAnnotTpe) + def applyOfAnnotatedCompanion(preSym: Symbol): Boolean = { + if (preSym != NoSymbol && preSym.isMethod && preSym.name == TermName("apply")) { + val owner = preSym.owner + val companionCls = + if (owner.isModuleClass) owner.companionClass + else if (owner.isModule) owner.moduleClass.companionClass + else NoSymbol + requiresExplicitGenerics(companionCls) + } else false + } + def analyzeTree(tree: Tree): Unit = analyzer.macroExpandee(tree) match { case `tree` | EmptyTree => tree match { - case t@TypeApply(pre, args) if requiresExplicitGenerics(pre.symbol) => + case t@TypeApply(pre, args) if requiresExplicitGenerics(pre.symbol) || applyOfAnnotatedCompanion(pre.symbol) => val inferredTypeParams = args.forall { case tt: TypeTree => tt.original == null || tt.original == EmptyTree case _ => false } if (inferredTypeParams) { - fail(t.pos, pre.symbol) + // If we're on companion.apply, report on the class symbol for clearer message + val targetSym = if (applyOfAnnotatedCompanion(pre.symbol)) pre.symbol.owner.companionClass else pre.symbol + fail(t.pos, targetSym) } case n@New(tpt) if requiresExplicitGenerics(tpt.tpe.typeSymbol) => val explicitTypeArgsProvided = tpt match { @@ -45,6 +58,7 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { case prevTree => analyzeTree(prevTree) } + analyzeTree(unit.body) } } From 3ff850d78ab54a666dbd17595c23421b7614dd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Tue, 23 Sep 2025 10:15:42 +0200 Subject: [PATCH 5/6] refactor: simplify applyOfAnnotatedCompanion method logic --- .../com/avsystem/commons/analyzer/ExplicitGenerics.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala index 207481dd5..b84f3a9e8 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala @@ -17,16 +17,15 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { def requiresExplicitGenerics(sym: Symbol): Boolean = sym != NoSymbol && (sym :: sym.overrides).flatMap(_.annotations).exists(_.tree.tpe <:< explicitGenericsAnnotTpe) - def applyOfAnnotatedCompanion(preSym: Symbol): Boolean = { - if (preSym != NoSymbol && preSym.isMethod && preSym.name == TermName("apply")) { + def applyOfAnnotatedCompanion(preSym: Symbol): Boolean = + preSym != NoSymbol && preSym.isMethod && preSym.name == TermName("apply") && { val owner = preSym.owner val companionCls = if (owner.isModuleClass) owner.companionClass else if (owner.isModule) owner.moduleClass.companionClass else NoSymbol requiresExplicitGenerics(companionCls) - } else false - } + } def analyzeTree(tree: Tree): Unit = analyzer.macroExpandee(tree) match { case `tree` | EmptyTree => From 250599bef7e74f9ad3c965365c0f680780524305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Wed, 25 Feb 2026 13:29:52 +0100 Subject: [PATCH 6/6] scalafmtAll --- .../commons/analyzer/ExplicitGenerics.scala | 16 +++++++++------- .../analyzer/ExplicitGenericsTest.scala | 19 ++++++++++--------- .../analyzer/ImplicitValueClassesTest.scala | 19 +++++++++---------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala index b84f3a9e8..1916b00c6 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala @@ -9,7 +9,6 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { lazy val explicitGenericsAnnotTpe = classType("com.avsystem.commons.annotation.explicitGenerics") - private def fail(pos: Position, symbol: Symbol): Unit = report(pos, s"$symbol requires that its type arguments are explicit (not inferred)") @@ -30,7 +29,9 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { def analyzeTree(tree: Tree): Unit = analyzer.macroExpandee(tree) match { case `tree` | EmptyTree => tree match { - case t@TypeApply(pre, args) if requiresExplicitGenerics(pre.symbol) || applyOfAnnotatedCompanion(pre.symbol) => + case t @ TypeApply(pre, args) + if requiresExplicitGenerics(pre.symbol) || applyOfAnnotatedCompanion(pre.symbol) => + val inferredTypeParams = args.forall { case tt: TypeTree => tt.original == null || tt.original == EmptyTree case _ => false @@ -40,12 +41,13 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { val targetSym = if (applyOfAnnotatedCompanion(pre.symbol)) pre.symbol.owner.companionClass else pre.symbol fail(t.pos, targetSym) } - case n@New(tpt) if requiresExplicitGenerics(tpt.tpe.typeSymbol) => + case n @ New(tpt) if requiresExplicitGenerics(tpt.tpe.typeSymbol) => val explicitTypeArgsProvided = tpt match { - case tt: TypeTree => tt.original match { - case AppliedTypeTree(_, args) if args.nonEmpty => true - case _ => false - } + case tt: TypeTree => + tt.original match { + case AppliedTypeTree(_, args) if args.nonEmpty => true + case _ => false + } case _ => false } if (!explicitTypeArgsProvided) { diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala index 48e1c5291..51252d089 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ExplicitGenericsTest.scala @@ -43,28 +43,30 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { } test("inferred in constructor should be rejected") { - assertErrors(2, + assertErrors( + 2, scala""" |import com.avsystem.commons.analyzer.TestUtils | |val x = new TestUtils.GenericClass() |val y = new TestUtils.GenericCaseClass(123) - |""".stripMargin) + |""".stripMargin, + ) } - test("inferred in apply when constructor marked should be rejected") { - assertErrors(1, + assertErrors( + 1, scala""" |import com.avsystem.commons.analyzer.TestUtils | |val x = TestUtils.GenericCaseClass(123) - |""".stripMargin) + |""".stripMargin, + ) } test("explicit in constructor should not be rejected") { - assertNoErrors( - scala""" + assertNoErrors(scala""" |import com.avsystem.commons.analyzer.TestUtils | |val x = new TestUtils.GenericClass[Int]() @@ -72,8 +74,7 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { } test("not marked should not be rejected") { - assertNoErrors( - scala""" + assertNoErrors(scala""" |def method[T](e: T) = e |class NotMarkedGenericClass[T] |final case class NotMarkedGenericCaseClass[T](arg: T) diff --git a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ImplicitValueClassesTest.scala b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ImplicitValueClassesTest.scala index 3db992e05..b67d985c3 100644 --- a/analyzer/src/test/scala/com/avsystem/commons/analyzer/ImplicitValueClassesTest.scala +++ b/analyzer/src/test/scala/com/avsystem/commons/analyzer/ImplicitValueClassesTest.scala @@ -10,7 +10,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |implicit final class GoodImplicitClass(val x: Int) extends AnyVal { | def double: Int = x * 2 |} - |""".stripMargin, + |""".stripMargin ) } @@ -42,7 +42,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |class RegularClass(val x: Int) { | def double: Int = x * 2 |} - |""".stripMargin, + |""".stripMargin ) } @@ -52,7 +52,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |implicit final class ImplicitClassWithImplicitParameter(val x: Int)(implicit dummy: DummyImplicit) { | def double: Int = x * 2 |} - |""".stripMargin, + |""".stripMargin ) } @@ -69,7 +69,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |implicit final class GoodImplicitClass2(val x: Int) extends SomeTrait { | def double: Int = x * 2 |} - |""".stripMargin, + |""".stripMargin ) } @@ -89,8 +89,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { } test("nested implicit class not extending AnyVal should pass") { - assertNoErrors( - scala""" + assertNoErrors(scala""" |class Outer { | implicit final class NestedImplicitClass(val x: Int) { | def double: Int = x * 2 @@ -105,7 +104,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |implicit final class ValueClass(x: com.avsystem.commons.misc.Timestamp) { | def sth: Long = x.millis |} - |""".stripMargin, + |""".stripMargin ) } @@ -116,7 +115,7 @@ final class ImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTest { |implicit final class LazyValueClassOps(lvc: => ValueClass) { | def someOp: Int = lvc.underlying |} - |""".stripMargin, + |""".stripMargin ) } @@ -194,7 +193,7 @@ final class NestedImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTes |class RegularClass(val x: Int) { | def double: Int = x * 2 |} - |""".stripMargin, + |""".stripMargin ) } @@ -213,7 +212,7 @@ final class NestedImplicitValueClassesSuite extends AnyFunSuite with AnalyzerTes | def double: Int = x * 2 | } |} - |""".stripMargin, + |""".stripMargin ) }