@@ -629,15 +629,15 @@ object RefChecks {
629629 // Verifying a concrete class has nothing unimplemented.
630630 if (! clazz.isOneOf(AbstractOrTrait )) {
631631 val abstractErrors = new mutable.ListBuffer [String ]
632+ val concreteClassUnimplementedMethodError = new mutable.ListBuffer [Message ]
632633 def abstractErrorMessage =
633634 // a little formatting polish
634635 if (abstractErrors.size <= 2 ) abstractErrors.mkString(" " )
635636 else abstractErrors.tail.mkString(abstractErrors.head + " :\n " , " \n " , " " )
636637
637- def abstractClassError (mustBeMixin : Boolean , msg : String ): Unit = {
638+ def abstractClassError (msg : String ): Unit = {
638639 def prelude = (
639640 if (clazz.isAnonymousClass || clazz.is(Module )) " object creation impossible"
640- else if (mustBeMixin) s " $clazz needs to be a mixin "
641641 else if clazz.is(Synthetic ) then " instance cannot be created"
642642 else s " $clazz needs to be abstract "
643643 ) + " , since"
@@ -717,37 +717,22 @@ object RefChecks {
717717 }
718718
719719 def stubImplementations : List [String ] = {
720- // Grouping missing methods by the declaring class
721- val regrouped = missingMethods.groupBy(_.owner).toList
722720 def membersStrings (members : List [Symbol ]) =
723721 members.sortBy(_.name.toString).map(_.asSeenFrom(clazz.thisType).showDcl + " = ???" )
724722
725- if (regrouped.tail.isEmpty)
726- membersStrings(regrouped.head._2)
727- else (regrouped.sortBy(_._1.name.toString()) flatMap {
728- case (owner, members) =>
729- (" // Members declared in " + owner.fullName) +: membersStrings(members) :+ " "
730- }).init
723+ membersStrings(missingMethods)
731724 }
732725
733- // If there are numerous missing methods, we presume they are aware of it and
734- // give them a nicely formatted set of method signatures for implementing.
735- if (missingMethods.size > 1 ) {
736- abstractClassError(false , " it has " + missingMethods.size + " unimplemented members." )
737- val preface =
738- """ |/** As seen from %s, the missing signatures are as follows.
739- | * For convenience, these are usable as stub implementations.
740- | */
741- |""" .stripMargin.format(clazz)
742- abstractErrors += stubImplementations.map(" " + _ + " \n " ).mkString(preface, " " , " " )
726+ if (missingMethods.size >= 1 ) {
727+ concreteClassUnimplementedMethodError += ConcreteClassHasUnimplementedMethods (clazz, missingMethods, createAddMissingMethodsAction(clazz, stubImplementations))
743728 return
744729 }
745730
746731 for (member <- missingMethods) {
747732 def showDclAndLocation (sym : Symbol ) =
748733 s " ${sym.showDcl} in ${sym.owner.showLocated}"
749734 def undefined (msg : String ) =
750- abstractClassError(false , s " ${showDclAndLocation(member)} is not defined $msg" )
735+ abstractClassError(s " ${showDclAndLocation(member)} is not defined $msg" )
751736 val underlying = member.underlyingSymbol
752737
753738 // Give a specific error message for abstract vars based on why it fails:
@@ -840,7 +825,7 @@ object RefChecks {
840825 val impl1 = clazz.thisType.nonPrivateMember(decl.name) // DEBUG
841826 report.log(i " ${impl1}: ${impl1.info}" ) // DEBUG
842827 report.log(i " ${clazz.thisType.memberInfo(decl)}" ) // DEBUG
843- abstractClassError(false , " there is a deferred declaration of " + infoString(decl) +
828+ abstractClassError(" there is a deferred declaration of " + infoString(decl) +
844829 " which is not implemented in a subclass" + err.abstractVarMessage(decl))
845830 }
846831 if (bc.asClass.superClass.is(Abstract ))
@@ -916,6 +901,8 @@ object RefChecks {
916901 if (abstractErrors.nonEmpty)
917902 report.error(abstractErrorMessage, clazz.srcPos)
918903
904+ concreteClassUnimplementedMethodError.foreach(report.error(_, clazz.srcPos))
905+
919906 checkMemberTypesOK()
920907 checkCaseClassInheritanceInvariant()
921908 }
@@ -1272,6 +1259,79 @@ object RefChecks {
12721259 case tp : NamedType if tp.prefix.typeSymbol != ctx.owner.enclosingClass =>
12731260 report.warning(UnqualifiedCallToAnyRefMethod (tree, tree.symbol), tree)
12741261 case _ => ()
1262+
1263+ private def createAddMissingMethodsAction (clazz : ClassSymbol , methods : List [String ])(using Context ): List [CodeAction ] = {
1264+ import dotty .tools .dotc .rewrites .Rewrites .ActionPatch
1265+ import dotty .tools .dotc .util .Spans
1266+ import dotty .tools .dotc .ast .untpd
1267+
1268+ val untypedTree = NavigateAST
1269+ .untypedPath(clazz.span)
1270+ .head
1271+ .asInstanceOf [untpd.Tree ]
1272+
1273+ val classSrcPos = clazz.srcPos
1274+ val content = classSrcPos.sourcePos.source.content()
1275+ val span = classSrcPos.endPos.span
1276+
1277+ val classText = new String (content.slice(untypedTree.span.start, untypedTree.span.end))
1278+ val classHasBraces = classText.contains(" {" ) && classText.contains(" }" )
1279+
1280+ // Indentation for inserted methods
1281+ val lineStart = content.lastIndexWhere(c => c == '\n ' , end = span.end - 1 ) + 1
1282+ val baseIndent = new String (content.slice(lineStart, span.end).takeWhile(c => c == ' ' || c == '\t ' ))
1283+ val indent = baseIndent + " "
1284+
1285+ val formattedMethods = methods.map(m => s " $indent$m" ).mkString(System .lineSeparator())
1286+
1287+ val isBracelessSyntax = untypedTree match
1288+ case untpd.TypeDef (_, tmpl : untpd.Template ) =>
1289+ ! classText.contains(" {" ) && tmpl.body.nonEmpty
1290+ case _ => false
1291+
1292+ if (classHasBraces) {
1293+ val insertBeforeBrace = untypedTree.sourcePos.withSpan(Span (untypedTree.span.end - 1 ))
1294+ val braceStart = classText.indexOf('{' )
1295+ val braceEnd = classText.lastIndexOf('}' )
1296+ val bodyBetweenBraces = classText.slice(braceStart + 1 , braceEnd)
1297+ val bodyIsEmpty = bodyBetweenBraces.forall(_.isWhitespace)
1298+ val bodyContainsNewLine = bodyBetweenBraces.contains(System .lineSeparator())
1299+
1300+ val prefix = if (bodyContainsNewLine) " " else System .lineSeparator()
1301+ val patchText =
1302+ prefix +
1303+ formattedMethods +
1304+ System .lineSeparator()
1305+
1306+ val patch = ActionPatch (insertBeforeBrace, patchText)
1307+ List (CodeAction (" Add missing methods" , None , List (patch)))
1308+ } else if (isBracelessSyntax) {
1309+ val insertAfterLastDef = untypedTree match
1310+ case untpd.TypeDef (_, tmpl : untpd.Template ) if tmpl.body.nonEmpty =>
1311+ val lastDef = tmpl.body.last
1312+ lastDef.sourcePos.withSpan(Span (lastDef.span.end))
1313+ case _ =>
1314+ untypedTree.sourcePos.withSpan(Span (untypedTree.span.end))
1315+
1316+ val patchText = System .lineSeparator() + formattedMethods
1317+
1318+ val patch = ActionPatch (insertAfterLastDef, patchText)
1319+ List (CodeAction (" Add missing methods" , None , List (patch)))
1320+ } else {
1321+ // Class has no body – add whole `{ ... }` after class header, same line
1322+ val insertAfterHeader = untypedTree.sourcePos.withSpan(Span (untypedTree.span.end))
1323+
1324+ val patchText =
1325+ " {" + System .lineSeparator() +
1326+ formattedMethods + System .lineSeparator() +
1327+ " }"
1328+
1329+ val patch = ActionPatch (insertAfterHeader, patchText)
1330+ List (CodeAction (" Add missing methods" , None , List (patch)))
1331+ }
1332+ }
1333+
1334+
12751335}
12761336import RefChecks .*
12771337
0 commit comments