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/5] 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/5] 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/5] 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/5] 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/5] 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 =>