@@ -24,6 +24,7 @@ import Annotations.Annotation
2424import Constants .Constant
2525import cc .{stripCapturing , CCState }
2626import cc .Mutability .isUpdateMethod
27+ import dotty .tools .dotc .util .Chars .{isLineBreakChar , isWhitespace }
2728
2829object RefChecks {
2930 import tpd .*
@@ -338,6 +339,29 @@ object RefChecks {
338339 val clazzNameEnd = clazz.srcPos.span.start + clazz.name.stripModuleClassSuffix.lastPart.length
339340 clazz.srcPos.sourcePos.copy(span = clazz.srcPos.span.withEnd(clazzNameEnd))
340341
342+ /** True when `clazz` is the synthetic anonymous class generated for an
343+ * enum case (either a simple case via `$new` or a parameterized one
344+ * marked with `EnumCase`). */
345+ def isEnumAnonCls = // courtesy of Checking.checkEnum
346+ clazz.isAnonymousClass
347+ && clazz.owner.isTerm
348+ && {
349+ clazz.owner.isAllOf(EnumCase )
350+ || (clazz.owner.name eq nme.DOLLAR_NEW ) && clazz.owner.isAllOf(Private | Synthetic )
351+ }
352+
353+ /** Positions at which to report a "needs to be abstract" / "object creation
354+ * impossible" error for `clazz`. Normally a single position pointing at
355+ * the class name; for enum-case anonymous classes the error is moved to
356+ * the case definition(s) so the user sees it on `case Foo` rather than on
357+ * the synthetic `$anon`. */
358+ def classErrorPositions : List [util.SrcPos ] =
359+ if ! isEnumAnonCls then clazzNamePos :: Nil
360+ else if clazz.owner.isAllOf(EnumCase ) then clazz.owner.srcPos :: Nil
361+ else
362+ val e = clazz.parentSyms.head
363+ e.children.filter(_.info.typeSymbol == e).map(_.srcPos)
364+
341365 def printMixinOverrideErrors (): Unit =
342366 mixinOverrideErrors.toList match {
343367 case Nil =>
@@ -646,15 +670,15 @@ object RefChecks {
646670 // Verifying a concrete class has nothing unimplemented.
647671 if (! clazz.isOneOf(AbstractOrTrait )) {
648672 val abstractErrors = new mutable.ListBuffer [String ]
673+ val concreteClassUnimplementedMethodError = new mutable.ListBuffer [Message ]
649674 def abstractErrorMessage =
650675 // a little formatting polish
651676 if (abstractErrors.size <= 2 ) abstractErrors.mkString(" " )
652677 else abstractErrors.tail.mkString(abstractErrors.head + " :\n " , " \n " , " " )
653678
654- def abstractClassError (mustBeMixin : Boolean , msg : String ): Unit = {
679+ def abstractClassError (msg : String ): Unit = {
655680 def prelude = (
656681 if (clazz.isAnonymousClass || clazz.is(Module )) " object creation impossible"
657- else if (mustBeMixin) s " $clazz needs to be a mixin "
658682 else if clazz.is(Synthetic ) then " instance cannot be created"
659683 else s " $clazz needs to be abstract "
660684 ) + " , since"
@@ -733,56 +757,28 @@ object RefChecks {
733757 .distinctBy(_.signature) // Avoid duplication for similar definitions (#19731)
734758 }
735759
736- /** Replace synthetic parameter names (x$0, x$1, ...) with
737- * dollar-free versions (x0, x1, ...) so that stub implementations
738- * are directly usable as valid Scala identifiers.
739- */
740- def replaceSyntheticParamNames (tp : Type ): Type = tp match
741- case mt : MethodType if mt.allParamNamesSynthetic =>
742- val newNames = mt.paramNames.zipWithIndex.map((_, i) => termName(" x" + i))
743- mt.derivedLambdaType(newNames, mt.paramInfos, replaceSyntheticParamNames(mt.resType))
744- case mt : MethodType =>
745- mt.derivedLambdaType(mt.paramNames, mt.paramInfos, replaceSyntheticParamNames(mt.resType))
746- case pt : PolyType =>
747- pt.derivedLambdaType(pt.paramNames, pt.paramInfos, replaceSyntheticParamNames(pt.resType))
748- case _ => tp
749-
750- def stubImplementations : List [String ] = {
751- // Grouping missing methods by the declaring class
752- val regrouped = missingMethods.groupBy(_.owner).toList
753- def membersStrings (members : List [Symbol ]) =
754- members.sortBy(_.name.toString).map: sym =>
755- val denot = sym.asSeenFrom(clazz.thisType)
756- denot.mapInfo(replaceSyntheticParamNames).showDcl + " = ???"
757-
758- if (regrouped.tail.isEmpty)
759- membersStrings(regrouped.head._2)
760- else (regrouped.sortBy(_._1.name.toString()) flatMap {
761- case (owner, members) =>
762- (" // Members declared in " + owner.fullName) +: membersStrings(members) :+ " "
763- }).init
764- }
760+ def stubImplementations : List [String ] =
761+ missingMethods.sortBy(_.name.toString).map: sym =>
762+ sym.asSeenFrom(clazz.thisType).mapInfo(_.withCleanParamNames).showDcl + " = ???"
765763
766- // If there are numerous missing methods, we presume they are aware of it and
767- // give them a nicely formatted set of method signatures for implementing.
768- if (missingMethods.size > 1 ) {
769- abstractClassError(false , " it has " + missingMethods.size + " unimplemented members." )
770- val preface =
771- """ |/** As seen from %s, the missing signatures are as follows.
772- | * For convenience, these are usable as stub implementations.
773- | */
774- |""" .stripMargin.format(clazz)
775- abstractErrors += stubImplementations.map(" " + _ + " \n " ).mkString(preface, " " , " " )
764+ if missingMethods.isEmpty then return
765+
766+ lazy val actions =
767+ createAddMissingMethodsAction(clazz, stubImplementations)
768+ ++ createMakeClassAbstractAction(clazz)
769+
770+ if missingMethods.size > 1 then
771+ concreteClassUnimplementedMethodError += ConcreteClassHasUnimplementedMethods (
772+ clazz, missingMethods, addendum = " " , actions)
776773 return
777- }
778774
779775 for (member <- missingMethods) {
780776 def showDclAndLocation (sym : Symbol ) =
781- s " ${sym.mapInfo(replaceSyntheticParamNames ).showDcl} in ${sym.owner.showLocated}"
777+ s " ${sym.mapInfo(_.withCleanParamNames ).showDcl} in ${sym.owner.showLocated}"
782778 def undefined (msg : String ) =
783- val notdefined = s " ${showDclAndLocation(member)} is not defined "
784- val text = if ! msg.isEmpty then s " $notdefined $msg " else notdefined
785- abstractClassError(mustBeMixin = false , text )
779+ val addendum = if msg.isEmpty then " " else s " $msg "
780+ concreteClassUnimplementedMethodError += ConcreteClassHasUnimplementedMethods (
781+ clazz, missingMethods, addendum, actions )
786782 val underlying = member.underlyingSymbol
787783
788784 // Give a specific error message for abstract vars based on why it fails:
@@ -875,7 +871,7 @@ object RefChecks {
875871 val impl1 = clazz.thisType.nonPrivateMember(decl.name) // DEBUG
876872 report.log(i " ${impl1}: ${impl1.info}" ) // DEBUG
877873 report.log(i " ${clazz.thisType.memberInfo(decl)}" ) // DEBUG
878- abstractClassError(false , " there is a deferred declaration of " + infoString(decl) +
874+ abstractClassError(" there is a deferred declaration of " + infoString(decl) +
879875 " which is not implemented in a subclass" + err.abstractVarMessage(decl))
880876 }
881877 if (bc.asClass.superClass.is(Abstract ))
@@ -945,25 +941,17 @@ object RefChecks {
945941 |This is a limitation that enables better GADT constraints in case class patterns""" .stripMargin
946942 do report.errorOrMigrationWarning(withExplain, clazz.srcPos, MigrationVersion .Scala2to3 )
947943 checkNoAbstractMembers()
948- if (abstractErrors.isEmpty)
944+ if (abstractErrors.isEmpty && concreteClassUnimplementedMethodError.isEmpty )
949945 checkNoAbstractDecls(clazz)
950946
951947 if abstractErrors.nonEmpty then
952- val isEnumAnonCls = // courtesy of Checking.checkEnum
953- clazz.isAnonymousClass
954- && clazz.owner.isTerm
955- && {
956- clazz.owner.isAllOf(EnumCase )
957- || (clazz.owner.name eq nme.DOLLAR_NEW ) && clazz.owner.isAllOf(Private | Synthetic )
958- }
959- if ! isEnumAnonCls then
960- report.error(abstractErrorMessage, clazzNamePos)
961- else if clazz.owner.isAllOf(EnumCase ) then
962- report.error(abstractErrorMessage, clazz.owner.srcPos)
963- else
964- val e = clazz.parentSyms.head
965- for child <- e.children if child.info.typeSymbol == e do // report all simple cases
966- report.error(abstractErrorMessage, child.srcPos)
948+ val msg = abstractErrorMessage
949+ classErrorPositions.foreach(report.error(msg, _))
950+ for
951+ message <- concreteClassUnimplementedMethodError
952+ pos <- classErrorPositions
953+ do
954+ report.error(message, pos)
967955
968956 checkMemberTypesOK()
969957 checkCaseClassInheritanceInvariant()
@@ -1349,7 +1337,111 @@ object RefChecks {
13491337 tree.tpe match
13501338 case tp : NamedType if tp.prefix.typeSymbol != ctx.enclosingClass =>
13511339 report.warning(UnqualifiedCallToAnyRefMethod (tree, tree.symbol), tree)
1340+ case _ => ()
1341+
1342+ private def createAddMissingMethodsAction (clazz : ClassSymbol , methods : List [String ])(using Context ): List [CodeAction ] =
1343+ // Synthetic classes (e.g. anonymous classes generated by macros) may have
1344+ // no corresponding node in the untyped AST. In that case there's nothing
1345+ // to anchor a source-level patch to, so we don't offer the action.
1346+ NavigateAST .untypedPath(clazz.span) match
1347+ case (untypedTree : untpd.Tree ) :: _ =>
1348+ addMissingMethodsActionPatch(clazz, methods, untypedTree)
1349+ case _ => Nil
1350+
1351+ /** Code action that prepends the `abstract` modifier to the class declaration.
1352+ *
1353+ * Only offered for regular classes that can legally become abstract. We skip:
1354+ * - modules (`object`s are implicitly final),
1355+ * - anonymous classes (no source-level keyword to modify),
1356+ * - case classes (cannot be abstract),
1357+ * - already-final classes (mutually exclusive with abstract),
1358+ * - synthetic classes (enum case singletons, given bodies, etc.).
1359+ *
1360+ * As with [[createAddMissingMethodsAction ]], if the class has no node in
1361+ * the untyped AST (e.g. macro-generated) we skip silently.
1362+ */
1363+ private def createMakeClassAbstractAction (clazz : ClassSymbol )(using Context ): List [CodeAction ] =
1364+ import dotty .tools .dotc .rewrites .Rewrites .ActionPatch
1365+
1366+ val ineligible =
1367+ clazz.is(Module ) || clazz.isAnonymousClass || clazz.is(Case ) ||
1368+ clazz.is(Final ) || clazz.is(Synthetic )
1369+ if ineligible then Nil
1370+ else NavigateAST .untypedPath(clazz.span) match
1371+ case (untypedTree : untpd.Tree ) :: _ =>
1372+ val insertPos = untypedTree.sourcePos.withSpan(Span (untypedTree.span.start))
1373+ val patch = ActionPatch (insertPos, " abstract " )
1374+ List (CodeAction (s " Make ` ${clazz.name.show}` abstract " , None , List (patch)))
1375+ case _ => Nil
1376+
1377+ private def addMissingMethodsActionPatch (
1378+ clazz : ClassSymbol ,
1379+ methods : List [String ],
1380+ untypedTree : untpd.Tree )(using Context ): List [CodeAction ] = {
1381+ import dotty .tools .dotc .rewrites .Rewrites .ActionPatch
1382+
1383+ val classSrcPos = clazz.srcPos
1384+ val content = classSrcPos.sourcePos.source.content()
1385+ val span = classSrcPos.endPos.span
1386+
1387+ val classText = new String (content.slice(untypedTree.span.start, untypedTree.span.end))
1388+ val classHasBraces = classText.contains(" {" ) && classText.contains(" }" )
1389+
1390+ // Indentation for inserted methods
1391+ val lineStart = content.lastIndexWhere(isLineBreakChar, end = span.end - 1 ) + 1
1392+ val baseIndent = new String (content.slice(lineStart, span.end).takeWhile(c => c == ' ' || c == '\t ' ))
1393+ val indent = baseIndent + " "
1394+
1395+ val formattedMethods = methods.map(m => s " $indent$m" ).mkString(System .lineSeparator())
1396+
1397+ val isBracelessSyntax = untypedTree match
1398+ case untpd.TypeDef (_, tmpl : untpd.Template ) =>
1399+ ! classText.contains(" {" ) && tmpl.body.nonEmpty
1400+ case _ => false
1401+
1402+ if (classHasBraces) {
1403+ val insertBeforeBrace = untypedTree.sourcePos.withSpan(Span (untypedTree.span.end - 1 ))
1404+ val braceStart = classText.indexOf('{' )
1405+ val braceEnd = classText.lastIndexOf('}' )
1406+ val bodyBetweenBraces = classText.slice(braceStart + 1 , braceEnd)
1407+ val bodyIsEmpty = bodyBetweenBraces.forall(_.isWhitespace)
1408+ val bodyContainsNewLine = bodyBetweenBraces.exists(isLineBreakChar)
1409+
1410+ val prefix = if (bodyContainsNewLine) " " else System .lineSeparator()
1411+ val patchText =
1412+ prefix +
1413+ formattedMethods +
1414+ System .lineSeparator()
1415+
1416+ val patch = ActionPatch (insertBeforeBrace, patchText)
1417+ List (CodeAction (" Add missing methods" , None , List (patch)))
1418+ } else if (isBracelessSyntax) {
1419+ val insertAfterLastDef = untypedTree match
1420+ case untpd.TypeDef (_, tmpl : untpd.Template ) if tmpl.body.nonEmpty =>
1421+ val lastDef = tmpl.body.last
1422+ lastDef.sourcePos.withSpan(Span (lastDef.span.end))
13521423 case _ =>
1424+ untypedTree.sourcePos.withSpan(Span (untypedTree.span.end))
1425+
1426+ val patchText = System .lineSeparator() + formattedMethods
1427+
1428+ val patch = ActionPatch (insertAfterLastDef, patchText)
1429+ List (CodeAction (" Add missing methods" , None , List (patch)))
1430+ } else {
1431+ // Class has no body – add whole `{ ... }` after class header, same line
1432+ val insertAfterHeader = untypedTree.sourcePos.withSpan(Span (untypedTree.span.end))
1433+
1434+ val patchText =
1435+ " {" + System .lineSeparator() +
1436+ formattedMethods + System .lineSeparator() +
1437+ " }"
1438+
1439+ val patch = ActionPatch (insertAfterHeader, patchText)
1440+ List (CodeAction (" Add missing methods" , None , List (patch)))
1441+ }
1442+ }
1443+
1444+
13531445}
13541446import RefChecks .*
13551447
0 commit comments