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 a69e17302..1916b00c6 100644 --- a/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala +++ b/analyzer/src/main/scala/com/avsystem/commons/analyzer/ExplicitGenerics.scala @@ -5,24 +5,53 @@ 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") - 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) + 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) + } + 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) { - report(t.pos, s"${pre.symbol} requires that its type arguments are explicit (not inferred)") + // 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 { + 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 _ => } @@ -30,6 +59,7 @@ class ExplicitGenerics(g: Global) extends AnalyzerRule(g, "explicitGenerics") { case prevTree => analyzeTree(prevTree) } + analyzeTree(unit.body) } } 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 e58417266..51252d089 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,48 @@ final class ExplicitGenericsTest extends AnyFunSuite with AnalyzerTest { |val x = TestUtils.genericMacro[Int](123) |""".stripMargin) } + + test("inferred in constructor should be rejected") { + 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, + ) + } + + test("explicit in constructor should not be rejected") { + assertNoErrors(scala""" + |import com.avsystem.commons.analyzer.TestUtils + | + |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/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 ) } 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..dfb82d724 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,10 @@ object TestUtils { def genericMethod[T](arg: T): T = arg @explicitGenerics def genericMacro[T](arg: T): T = macro genericMacroImpl[T] + + @explicitGenerics + class GenericClass[T] + + @explicitGenerics + case class GenericCaseClass[T](arg: T) }