diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index d71a6329e8b0..51e601441ead 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -67,6 +67,8 @@ object desugar { */ val TrailingForMap: Property.Key[Unit] = Property.StickyKey() + val WasTypedInfix: Property.Key[Unit] = Property.StickyKey() + /** What static check should be applied to a Match? */ enum MatchCheck { case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom @@ -1667,10 +1669,12 @@ object desugar { case _ => Apply(sel, arg :: Nil) - if op.name.isRightAssocOperatorName then + val apply = if op.name.isRightAssocOperatorName then makeOp(right, left, Span(op.span.start, right.span.end)) else makeOp(left, right, Span(left.span.start, op.span.end, op.span.start)) + apply.pushAttachment(WasTypedInfix, ()) + return apply } /** Translate throws type `A throws E1 | ... | En` to diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 9b032e1e7539..4cc8411a4fce 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -14,6 +14,7 @@ import printing.Highlighting.* import printing.Formatting import ErrorMessageID.* import ast.Trees +import ast.desugar import config.{Feature, MigrationVersion, ScalaVersion} import transform.patmat.Space import transform.patmat.SpaceEngine @@ -300,6 +301,11 @@ extends NotFoundMsg(MissingIdentID) { class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tree], addenda: => String*)(using Context) extends TypeMismatchMsg(found, expected)(TypeMismatchID): + private val shouldSuggestNN = + if ctx.mode.is(Mode.SafeNulls) && expected.isValueType then + found frozen_<:< OrNull(expected) + else false + def msg(using Context) = // replace constrained TypeParamRefs and their typevars by their bounds where possible // and the bounds are not f-bounds. @@ -360,6 +366,26 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre val treeStr = inTree.map(x => s"\nTree:\n\n${x.show}\n").getOrElse("") treeStr + "\n" + super.explain + override def actions(using Context) = + inTree match { + case Some(tree) if shouldSuggestNN => + val content = tree.source.content().slice(tree.srcPos.startPos.start, tree.srcPos.endPos.end).mkString + val replacement = tree match + case a @ Apply(_, _) if !a.hasAttachment(desugar.WasTypedInfix) => + content + ".nn" + case _ @ (Select(_, _) | Ident(_)) => content + ".nn" + case _ => "(" + content + ").nn" + List( + CodeAction(title = """Add .nn""", + description = None, + patches = List( + ActionPatch(tree.srcPos.sourcePos, replacement) + ) + ) + ) + case _ => + List() + } end TypeMismatch class NotAMember(site: Type, val name: Name, selected: String, proto: Type, addendum: => String = "")(using Context) diff --git a/compiler/test/dotty/tools/dotc/reporting/CodeActionTest.scala b/compiler/test/dotty/tools/dotc/reporting/CodeActionTest.scala index 91074110389e..91ff958a7889 100644 --- a/compiler/test/dotty/tools/dotc/reporting/CodeActionTest.scala +++ b/compiler/test/dotty/tools/dotc/reporting/CodeActionTest.scala @@ -179,6 +179,130 @@ class CodeActionTest extends DottyTest: ctxx = ctxx ) + @Test def addNN1 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """val s: String|Null = ??? + | val t: String = s""".stripMargin, + title = "Add .nn", + expected = + """val s: String|Null = ??? + | val t: String = s.nn""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN2 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String): String | Null = null + |} + | val s: String = ??? + | val t: String = s q s""".stripMargin, + title = "Add .nn", + expected = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String): String | Null = null + |} + | val s: String = ??? + | val t: String = (s q s).nn""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN3 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String, s3: String): String | Null = null + |} + | val s: String = ??? + | val t: String = s q (s, s)""".stripMargin, + title = "Add .nn", + expected = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String, s3: String): String | Null = null + |} + | val s: String = ??? + | val t: String = (s q (s, s)).nn""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN4 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String, s3: String): String | Null = null + |} + | val s: String = ??? + | val t: String = s.q(s, s)""".stripMargin, + title = "Add .nn", + expected = + """implicit class infixOpTest(val s1: String) extends AnyVal { + | def q(s2: String, s3: String): String | Null = null + |} + | val s: String = ??? + | val t: String = s.q(s, s).nn""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN5 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """val s: String | Null = ??? + |val t: String = s match { + | case _: String => "foo" + | case _ => s + |}""".stripMargin, + title = "Add .nn", + expected = + """val s: String | Null = ??? + |val t: String = s match { + | case _: String => "foo" + | case _ => s.nn + |}""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN6 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """val s: String | Null = ??? + |val t: String = if (s != null) "foo" else s""".stripMargin, + title = "Add .nn", + expected = + """val s: String | Null = ??? + |val t: String = if (s != null) "foo" else s.nn""".stripMargin, + ctxx = ctxx + ) + + @Test def addNN7 = + val ctxx = newContext + ctxx.setSetting(ctxx.settings.YexplicitNulls, true) + checkCodeAction( + code = + """given ctx: String | Null = null + |def f(using c: String): String = c + |val s: String = f(using ctx)""".stripMargin, + title = "Add .nn", + expected = + """given ctx: String | Null = null + |def f(using c: String): String = c + |val s: String = f(using ctx.nn)""".stripMargin, + ctxx = ctxx + ) + // Make sure we're not using the default reporter, which is the ConsoleReporter, // meaning they will get reported in the test run and that's it. private def newContext =