From 60cd63dc7dcc4fbf1d7309fabd0f8e161fa64391 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 30 Mar 2025 18:56:46 -0700 Subject: [PATCH 1/3] Check trivial trait args --- .../scala/tools/nsc/ast/TreeBrowsers.scala | 2 +- .../scala/tools/nsc/typechecker/Namers.scala | 56 ++++++------ .../scala/tools/nsc/typechecker/Typers.scala | 86 ++++++++++--------- test/files/neg/t6805.check | 13 +++ test/files/neg/t6805.scala | 14 +++ test/files/pos/t6666d.scala | 2 +- .../run/src-2/tastytest/TestGreeting.scala | 2 +- .../tasty/run/src-2/tastytest/TestInner.scala | 2 +- .../run/src-2/tastytest/TestReader.scala | 2 +- 9 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 test/files/neg/t6805.check create mode 100644 test/files/neg/t6805.scala diff --git a/src/compiler/scala/tools/nsc/ast/TreeBrowsers.scala b/src/compiler/scala/tools/nsc/ast/TreeBrowsers.scala index a309eecfe874..d08554308254 100644 --- a/src/compiler/scala/tools/nsc/ast/TreeBrowsers.scala +++ b/src/compiler/scala/tools/nsc/ast/TreeBrowsers.scala @@ -189,7 +189,7 @@ abstract class TreeBrowsers { } } - jTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { + jTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener { def valueChanged(e: javax.swing.event.TreeSelectionEvent): Unit = { textArea.setText(e.getPath().getLastPathComponent().toString) infoPanel.update(e.getPath().getLastPathComponent()) diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index 036491205b2e..bb4651554f16 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -1182,37 +1182,35 @@ trait Namers extends MethodSynthesis { private def templateSig(templ: Template): Type = { val clazz = context.owner - val parentTrees = typer.typedParentTypes(templ) - val pending = mutable.ListBuffer[AbsTypeError]() - parentTrees foreach { tpt => - val ptpe = tpt.tpe - if (!ptpe.isError && !phase.erasedTypes) { - val psym = ptpe.typeSymbol - if (psym.isSealed) { - val sameSourceFile = context.unit.source.file == psym.sourceFile - val okChild = - if (psym.isJava) - psym.attachments.get[PermittedSubclassSymbols] match { - case Some(permitted) => permitted.permits.exists(_ == clazz) - case _ => sameSourceFile - } - else - sameSourceFile - if (okChild) - psym.addChild(clazz) - else - pending += ParentSealedInheritanceError(tpt, psym) + val parents = + typer.typedParentTypes(templ).map { tpt => + val ptpe = tpt.tpe + if (ptpe.isError) + AnyRefTpe + else { + if (!phase.erasedTypes) { + val psym = ptpe.typeSymbol + if (psym.isSealed) { + val sameSourceFile = context.unit.source.file == psym.sourceFile + val okChild = + if (psym.isJava) + psym.attachments.get[PermittedSubclassSymbols] match { + case Some(permitted) => permitted.permits.exists(_ == clazz) + case _ => sameSourceFile + } + else + sameSourceFile + if (okChild) + psym.addChild(clazz) + else + ErrorUtils.issueTypeError(ParentSealedInheritanceError(tpt, psym)) + } + if (psym.isLocalToBlock && psym.isClass) + psym.addChild(clazz) + } + ptpe } - if (psym.isLocalToBlock && psym.isClass) - psym.addChild(clazz) } - } - pending.foreach(ErrorUtils.issueTypeError) - - val parents = { - def checkParent(tpt: Tree): Type = if (tpt.tpe.isError) AnyRefTpe else tpt.tpe - parentTrees map checkParent - } enterSelf(templ.self) diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index 4e0ae1789b37..ca47c723ecfd 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -1582,12 +1582,12 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper * (3 times from the typer) * */ - private def typedParentType(encodedtpt: Tree, templ: Template, inMixinPosition: Boolean): Tree = { + private def typedParentType(encodedtpt: Tree, templ: Template, inMixinPosition: Boolean, isAnonClass: Boolean): Tree = { val app @ treeInfo.Applied(core, _, argss) = treeInfo.dissectApplied(encodedtpt) val decodedtpt = app.callee val argssAreTrivial = argss == Nil || argss == ListOfNil - // we cannot avoid cyclic references with `initialize` here, because when type macros arrive, + // we cannot avoid cyclic references with `initialize` here, because when type macros arrive [sic], // we'll have to check the probe for isTypeMacro anyways. // therefore I think it's reasonable to trade a more specific "inherits itself" error // for a generic, yet understandable "cyclic reference" error @@ -1596,39 +1596,40 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper if (p == null) NoSymbol else p.initialize } - - def cookIfNeeded(tpt: Tree) = if (context.unit.isJava) tpt modifyType rawToExistential else tpt - cookIfNeeded(if (probe.isTrait || inMixinPosition) { - if (!argssAreTrivial) { - if (probe.isTrait) ConstrArgsInParentWhichIsTraitError(encodedtpt, probe) - else () // a class in a mixin position - this warrants an error in `validateParentClasses` - // therefore here we do nothing, e.g. don't check that the # of ctor arguments - // matches the # of ctor parameters or stuff like that + val tpt = + if (probe.isTrait || inMixinPosition) { + if (probe.isTrait && inMixinPosition && !isAnonClass && !argss.isEmpty) + ConstrArgsInParentWhichIsTraitError(encodedtpt, probe) + //if (!probe.isTrait) + // a class in a mixin position - this warrants an error in `validateParentClasses` + // therefore here we do nothing, e.g. don't check that the # of ctor arguments + // matches the # of ctor parameters or stuff like that + typedType(decodedtpt) } - typedType(decodedtpt) - } else { - val supertpt = typedTypeConstructor(decodedtpt) - val supertparams = if (supertpt.hasSymbolField) supertpt.symbol.typeParams else Nil - def inferParentTypeArgs: Tree = { - typedPrimaryConstrBody(templ) { - val supertpe = PolyType(supertparams, appliedType(supertpt.tpe, supertparams map (_.tpeHK))) - val supercall = New(supertpe, mmap(argss)(_.duplicate)) - val treeInfo.Applied(Select(ctor, nme.CONSTRUCTOR), _, _) = supercall: @unchecked - ctor setType supertpe // this is an essential hack, otherwise it will occasionally fail to typecheck - atPos(supertpt.pos.focus)(supercall) - } match { - case EmptyTree => MissingTypeArgumentsParentTpeError(supertpt); supertpt - case tpt => TypeTree(tpt.tpe) setPos supertpt.pos // scala/bug#7224: don't .focus positions of the TypeTree of a parent that exists in source + else { + val supertpt = typedTypeConstructor(decodedtpt) + val supertparams = if (supertpt.hasSymbolField) supertpt.symbol.typeParams else Nil + def inferParentTypeArgs: Tree = { + typedPrimaryConstrBody(templ) { + val supertpe = PolyType(supertparams, appliedType(supertpt.tpe, supertparams map (_.tpeHK))) + val supercall = New(supertpe, mmap(argss)(_.duplicate)) + val treeInfo.Applied(Select(ctor, nme.CONSTRUCTOR), _, _) = supercall: @unchecked + ctor setType supertpe // this is an essential hack, otherwise it will occasionally fail to typecheck + atPos(supertpt.pos.focus)(supercall) + } match { + case EmptyTree => MissingTypeArgumentsParentTpeError(supertpt); supertpt + case tpt => TypeTree(tpt.tpe) setPos supertpt.pos + // scala/bug#7224: don't .focus positions of the TypeTree of a parent that exists in source + } } - } + val supertptWithTargs = if (supertparams.isEmpty || context.unit.isJava) supertpt else inferParentTypeArgs - val supertptWithTargs = if (supertparams.isEmpty || context.unit.isJava) supertpt else inferParentTypeArgs - - // this is the place where we tell the typer what argss should be used for the super call - // if argss are nullary or empty, then (see the docs for `typedPrimaryConstrBody`) - // the super call dummy is already good enough, so we don't need to do anything - if (argssAreTrivial) supertptWithTargs else supertptWithTargs updateAttachment SuperArgsAttachment(argss) - }) + // this is the place where we tell the typer what argss should be used for the super call + // if argss are nullary or empty, then (see the docs for `typedPrimaryConstrBody`) + // the super call dummy is already good enough, so we don't need to do anything + if (argssAreTrivial) supertptWithTargs else supertptWithTargs updateAttachment SuperArgsAttachment(argss) + } + if (context.unit.isJava) tpt.modifyType(rawToExistential) else tpt } /** Typechecks the mishmash of trees that happen to be stuffed into the primary constructor of a given template. @@ -1714,7 +1715,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper * If the first parent is a trait, prepend its supertype to the list until it's a class. */ private def normalizeFirstParent(parents: List[Tree]): List[Tree] = { - @annotation.tailrec + @tailrec def explode0(parents: List[Tree]): List[Tree] = { val supertpt :: rest = parents: @unchecked // parents is always non-empty here - it only grows if (supertpt.tpe.typeSymbol == AnyClass) { @@ -1744,22 +1745,29 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper * So we strip the duplicates before typer. */ private def fixDuplicateSyntheticParents(parents: List[Tree]): List[Tree] = parents match { - case Nil => Nil - case x :: xs => + case x :: xs => val sym = x.symbol x :: fixDuplicateSyntheticParents( if (isPossibleSyntheticParent(sym)) xs.filter(_.symbol != sym) else xs ) + case nil => Nil } def typedParentTypes(templ: Template): List[Tree] = templ.parents match { case Nil => List(atPos(templ.pos)(TypeTree(AnyRefTpe))) - case first :: rest => + case parents => + val isAnonClass = !context.owner.isAnonymousClass // permit new T() {} syntax + def loop(parents: List[Tree], inMixinPosition: Boolean): List[Tree] = + parents match { + case parent :: parents => + typedParentType(parent, templ, inMixinPosition = inMixinPosition, isAnonClass = isAnonClass) :: + loop(parents, inMixinPosition = true) + case _ => Nil + } try { - val supertpts = fixDuplicateSyntheticParents(normalizeFirstParent( - typedParentType(first, templ, inMixinPosition = false) +: - (rest map (typedParentType(_, templ, inMixinPosition = true))))) + val tpts0 = loop(parents, inMixinPosition = false) + val supertpts = fixDuplicateSyntheticParents(normalizeFirstParent(tpts0)) // if that is required to infer the targs of a super call // typedParentType calls typedPrimaryConstrBody to do the inferring typecheck diff --git a/test/files/neg/t6805.check b/test/files/neg/t6805.check new file mode 100644 index 000000000000..a9a50ab137c8 --- /dev/null +++ b/test/files/neg/t6805.check @@ -0,0 +1,13 @@ +t6805.scala:4: error: trait T is a trait; does not take constructor arguments +class C extends T() // error + ^ +t6805.scala:7: error: trait T is a trait; does not take constructor arguments +class Y extends X with T() // error + ^ +t6805.scala:11: error: trait T is abstract; cannot be instantiated + def u: T = new T() // error + ^ +t6805.scala:12: error: trait T is a trait; does not take constructor arguments + def v: T = new X with T() // error + ^ +4 errors diff --git a/test/files/neg/t6805.scala b/test/files/neg/t6805.scala new file mode 100644 index 000000000000..b8ba4cb816fd --- /dev/null +++ b/test/files/neg/t6805.scala @@ -0,0 +1,14 @@ + +trait T + +class C extends T() // error + +class X +class Y extends X with T() // error + +object funcs { + def t: T = new T() {} // no error, permissive for Java anon syntax, just because + def u: T = new T() // error + def v: T = new X with T() // error + def w: T = new T {} +} diff --git a/test/files/pos/t6666d.scala b/test/files/pos/t6666d.scala index 49a688f91b23..3e51669d45fc 100644 --- a/test/files/pos/t6666d.scala +++ b/test/files/pos/t6666d.scala @@ -4,7 +4,7 @@ import scala.math.Ordering class Test[K](param:TreeMap[K,Int]){ def this() = this({ - implicit object TreeOrd extends Ordering[K](){ + implicit val TreeOrd: Ordering[K] = new Ordering[K] { def compare(a: K, b: K) = { -1 } diff --git a/test/tasty/run/src-2/tastytest/TestGreeting.scala b/test/tasty/run/src-2/tastytest/TestGreeting.scala index 6dd74c2e21bd..709bd2429cb6 100644 --- a/test/tasty/run/src-2/tastytest/TestGreeting.scala +++ b/test/tasty/run/src-2/tastytest/TestGreeting.scala @@ -6,6 +6,6 @@ object TestGreeting extends Suite("TestGreeting") { final val greeting = "Hello, World!" } - test(assert(new Greeter with Hello().accessGreeting === "Hello, World!")) + test(assert((new Greeter with Hello).accessGreeting === "Hello, World!")) } diff --git a/test/tasty/run/src-2/tastytest/TestInner.scala b/test/tasty/run/src-2/tastytest/TestInner.scala index 2556a1304d87..696f8c2e27c1 100644 --- a/test/tasty/run/src-2/tastytest/TestInner.scala +++ b/test/tasty/run/src-2/tastytest/TestInner.scala @@ -3,6 +3,6 @@ package tastytest object TestInner extends Suite("TestInner") { test(assert(Inner.Foo.Bar != null)) - test(assert(new Inner.Foo(){}.isInstanceOf[Inner.Foo])) + test(assert(new Inner.Foo {}.isInstanceOf[Inner.Foo])) } diff --git a/test/tasty/run/src-2/tastytest/TestReader.scala b/test/tasty/run/src-2/tastytest/TestReader.scala index 4a7019600953..c49f68357123 100644 --- a/test/tasty/run/src-2/tastytest/TestReader.scala +++ b/test/tasty/run/src-2/tastytest/TestReader.scala @@ -2,7 +2,7 @@ package tastytest object TestReader extends Suite("TestReader") { - implicit def mkReaderMonad[Ctx]: Reader[Ctx] = new Reader[Ctx]() {} + implicit def mkReaderMonad[Ctx]: Reader[Ctx] = new Reader[Ctx] {} def pureToString[F[_], A](fa: F[A])(implicit F: Monad[F]): F[String] = F.flatMap(fa)(a => F.pure(a.toString)) From dee9caca3e381443f27be153c9eb0962114dc5b3 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Fri, 4 Apr 2025 16:18:01 -0700 Subject: [PATCH 2/3] -Wtrait-args --- src/compiler/scala/tools/nsc/Reporting.scala | 3 ++- .../scala/tools/nsc/settings/Warnings.scala | 1 + .../scala/tools/nsc/typechecker/Typers.scala | 12 ++++++++---- test/files/neg/t6805.check | 13 ++++++------- test/files/neg/t6805.scala | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/compiler/scala/tools/nsc/Reporting.scala b/src/compiler/scala/tools/nsc/Reporting.scala index 70f029497c16..ccdc252f1ee6 100644 --- a/src/compiler/scala/tools/nsc/Reporting.scala +++ b/src/compiler/scala/tools/nsc/Reporting.scala @@ -608,7 +608,8 @@ object Reporting { WFlagSelfImplicit, WFlagUnnamedBooleanLiteral, WFlagTostringInterpolated, - WFlagValueDiscard + WFlagValueDiscard, + WFlagTraitArgs = wflag() sealed class Unused extends WarningCategory { diff --git a/src/compiler/scala/tools/nsc/settings/Warnings.scala b/src/compiler/scala/tools/nsc/settings/Warnings.scala index a04be1411d18..9e7a2f718712 100644 --- a/src/compiler/scala/tools/nsc/settings/Warnings.scala +++ b/src/compiler/scala/tools/nsc/settings/Warnings.scala @@ -131,6 +131,7 @@ trait Warnings { val warnToString = BooleanSetting("-Wtostring-interpolated", "Warn when a standard interpolator uses toString.") val warnMultiargInfix = BooleanSetting("-Wmultiarg-infix", "Infix operator was defined or used with multiarg operand.") def multiargInfix = warnMultiargInfix.value + val warnTraitParens = BooleanSetting("-Wtrait-args", "Warn when a trait has empty parens.") object PerformanceWarnings extends MultiChoiceEnumeration { val Captured = Choice("captured", "Modification of var in closure causes boxing.") diff --git a/src/compiler/scala/tools/nsc/typechecker/Typers.scala b/src/compiler/scala/tools/nsc/typechecker/Typers.scala index ca47c723ecfd..0d3f8fad7e32 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Typers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Typers.scala @@ -18,7 +18,7 @@ import scala.annotation._ import scala.collection.mutable, mutable.{ArrayBuffer, ListBuffer} import scala.reflect.internal.{Chars, TypesStats} import scala.reflect.internal.util.{CodeAction, FreshNameCreator, ListOfNil, Statistics} -import scala.tools.nsc.Reporting.{MessageFilter, Suppression, WConf, WarningCategory}, WarningCategory.Scala3Migration +import scala.tools.nsc.Reporting.{MessageFilter, Suppression, WConf, WarningCategory}, WarningCategory._ import scala.util.chaining._ import symtab.Flags._ import Mode._ @@ -1598,8 +1598,12 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper } val tpt = if (probe.isTrait || inMixinPosition) { - if (probe.isTrait && inMixinPosition && !isAnonClass && !argss.isEmpty) - ConstrArgsInParentWhichIsTraitError(encodedtpt, probe) + if (probe.isTrait) { + if (!argssAreTrivial) + ConstrArgsInParentWhichIsTraitError(encodedtpt, probe) + else if (settings.warnTraitParens.value && (!isAnonClass || inMixinPosition) && !argss.isEmpty) + context.warning(encodedtpt.pos, "omit parens for trait which takes no arguments", WFlagTraitArgs) + } //if (!probe.isTrait) // a class in a mixin position - this warrants an error in `validateParentClasses` // therefore here we do nothing, e.g. don't check that the # of ctor arguments @@ -1757,7 +1761,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper def typedParentTypes(templ: Template): List[Tree] = templ.parents match { case Nil => List(atPos(templ.pos)(TypeTree(AnyRefTpe))) case parents => - val isAnonClass = !context.owner.isAnonymousClass // permit new T() {} syntax + val isAnonClass = context.owner.isAnonymousClass // permit new T() {} syntax def loop(parents: List[Tree], inMixinPosition: Boolean): List[Tree] = parents match { case parent :: parents => diff --git a/test/files/neg/t6805.check b/test/files/neg/t6805.check index a9a50ab137c8..29848427060e 100644 --- a/test/files/neg/t6805.check +++ b/test/files/neg/t6805.check @@ -1,13 +1,12 @@ -t6805.scala:4: error: trait T is a trait; does not take constructor arguments +t6805.scala:5: warning: omit parens for trait which takes no arguments class C extends T() // error ^ -t6805.scala:7: error: trait T is a trait; does not take constructor arguments +t6805.scala:8: warning: omit parens for trait which takes no arguments class Y extends X with T() // error ^ -t6805.scala:11: error: trait T is abstract; cannot be instantiated - def u: T = new T() // error - ^ -t6805.scala:12: error: trait T is a trait; does not take constructor arguments +t6805.scala:12: warning: omit parens for trait which takes no arguments def v: T = new X with T() // error ^ -4 errors +error: No warnings can be incurred under -Werror. +3 warnings +1 error diff --git a/test/files/neg/t6805.scala b/test/files/neg/t6805.scala index b8ba4cb816fd..6cb690d9cd5a 100644 --- a/test/files/neg/t6805.scala +++ b/test/files/neg/t6805.scala @@ -1,3 +1,4 @@ +//> using options -Wtrait-args -Werror trait T @@ -8,7 +9,6 @@ class Y extends X with T() // error object funcs { def t: T = new T() {} // no error, permissive for Java anon syntax, just because - def u: T = new T() // error def v: T = new X with T() // error - def w: T = new T {} + def w: T = new T {} // correct in every way } From c54bd42484f4c113cf431c0a4b0e82f629f5515b Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Fri, 4 Apr 2025 22:21:34 -0700 Subject: [PATCH 3/3] Test status quo trait args --- test/files/neg/t6805b.check | 13 +++++++++++++ test/files/neg/t6805b.scala | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 test/files/neg/t6805b.check create mode 100644 test/files/neg/t6805b.scala diff --git a/test/files/neg/t6805b.check b/test/files/neg/t6805b.check new file mode 100644 index 000000000000..66f2ed3e301a --- /dev/null +++ b/test/files/neg/t6805b.check @@ -0,0 +1,13 @@ +t6805b.scala:6: error: trait T is a trait; does not take constructor arguments +class D extends T(42) // error + ^ +t6805b.scala:10: error: trait T is a trait; does not take constructor arguments +class Z extends X with T(42) // error + ^ +t6805b.scala:14: error: trait T is a trait; does not take constructor arguments + def u: T = new T(42) {} // error + ^ +t6805b.scala:16: error: trait T is a trait; does not take constructor arguments + def w: T = new X with T(42) // error + ^ +4 errors diff --git a/test/files/neg/t6805b.scala b/test/files/neg/t6805b.scala new file mode 100644 index 000000000000..84c301282c3b --- /dev/null +++ b/test/files/neg/t6805b.scala @@ -0,0 +1,17 @@ +// status quo + +trait T + +class C extends T() // ok +class D extends T(42) // error + +class X +class Y extends X with T() // ok +class Z extends X with T(42) // error + +object funcs { + def t: T = new T() {} // no error, permissive for Java anon syntax, just because + def u: T = new T(42) {} // error + def v: T = new X with T() // ok + def w: T = new X with T(42) // error +}