Skip to content

Commit d860725

Browse files
authored
Add quick fix to add .nn (#23598)
An extension to #23461 that adds a quick fix to add a .nn. For example, if the code were ```scala val t: String | Null = ??? val s: String = t ``` the quick fix would transform the code to: ```scala val t: String | Null = ??? val s: String = t.nn ```
2 parents 55ab8d0 + bda8348 commit d860725

File tree

3 files changed

+155
-1
lines changed

3 files changed

+155
-1
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ object desugar {
6767
*/
6868
val TrailingForMap: Property.Key[Unit] = Property.StickyKey()
6969

70+
val WasTypedInfix: Property.Key[Unit] = Property.StickyKey()
71+
7072
/** What static check should be applied to a Match? */
7173
enum MatchCheck {
7274
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -1720,10 +1722,12 @@ object desugar {
17201722
case _ =>
17211723
Apply(sel, arg :: Nil)
17221724

1723-
if op.name.isRightAssocOperatorName then
1725+
val apply = if op.name.isRightAssocOperatorName then
17241726
makeOp(right, left, Span(op.span.start, right.span.end))
17251727
else
17261728
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
1729+
apply.pushAttachment(WasTypedInfix, ())
1730+
return apply
17271731
}
17281732

17291733
/** Translate throws type `A throws E1 | ... | En` to

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import printing.Highlighting.*
1414
import printing.Formatting
1515
import ErrorMessageID.*
1616
import ast.Trees
17+
import ast.desugar
1718
import config.{Feature, MigrationVersion, ScalaVersion}
1819
import transform.patmat.Space
1920
import transform.patmat.SpaceEngine
@@ -300,6 +301,11 @@ extends NotFoundMsg(MissingIdentID) {
300301
class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tree], addenda: => String*)(using Context)
301302
extends TypeMismatchMsg(found, expected)(TypeMismatchID):
302303

304+
private val shouldSuggestNN =
305+
if ctx.mode.is(Mode.SafeNulls) && expected.isValueType then
306+
found frozen_<:< OrNull(expected)
307+
else false
308+
303309
def msg(using Context) =
304310
// replace constrained TypeParamRefs and their typevars by their bounds where possible
305311
// and the bounds are not f-bounds.
@@ -360,6 +366,26 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre
360366
val treeStr = inTree.map(x => s"\nTree:\n\n${x.show}\n").getOrElse("")
361367
treeStr + "\n" + super.explain
362368

369+
override def actions(using Context) =
370+
inTree match {
371+
case Some(tree) if shouldSuggestNN =>
372+
val content = tree.source.content().slice(tree.srcPos.startPos.start, tree.srcPos.endPos.end).mkString
373+
val replacement = tree match
374+
case a @ Apply(_, _) if !a.hasAttachment(desugar.WasTypedInfix) =>
375+
content + ".nn"
376+
case _ @ (Select(_, _) | Ident(_)) => content + ".nn"
377+
case _ => "(" + content + ").nn"
378+
List(
379+
CodeAction(title = """Add .nn""",
380+
description = None,
381+
patches = List(
382+
ActionPatch(tree.srcPos.sourcePos, replacement)
383+
)
384+
)
385+
)
386+
case _ =>
387+
List()
388+
}
363389
end TypeMismatch
364390

365391
class NotAMember(site: Type, val name: Name, selected: String, proto: Type, addendum: => String = "")(using Context)

compiler/test/dotty/tools/dotc/reporting/CodeActionTest.scala

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,130 @@ class CodeActionTest extends DottyTest:
179179
ctxx = ctxx
180180
)
181181

182+
@Test def addNN1 =
183+
val ctxx = newContext
184+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
185+
checkCodeAction(
186+
code =
187+
"""val s: String|Null = ???
188+
| val t: String = s""".stripMargin,
189+
title = "Add .nn",
190+
expected =
191+
"""val s: String|Null = ???
192+
| val t: String = s.nn""".stripMargin,
193+
ctxx = ctxx
194+
)
195+
196+
@Test def addNN2 =
197+
val ctxx = newContext
198+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
199+
checkCodeAction(
200+
code =
201+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
202+
| def q(s2: String): String | Null = null
203+
|}
204+
| val s: String = ???
205+
| val t: String = s q s""".stripMargin,
206+
title = "Add .nn",
207+
expected =
208+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
209+
| def q(s2: String): String | Null = null
210+
|}
211+
| val s: String = ???
212+
| val t: String = (s q s).nn""".stripMargin,
213+
ctxx = ctxx
214+
)
215+
216+
@Test def addNN3 =
217+
val ctxx = newContext
218+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
219+
checkCodeAction(
220+
code =
221+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
222+
| def q(s2: String, s3: String): String | Null = null
223+
|}
224+
| val s: String = ???
225+
| val t: String = s q (s, s)""".stripMargin,
226+
title = "Add .nn",
227+
expected =
228+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
229+
| def q(s2: String, s3: String): String | Null = null
230+
|}
231+
| val s: String = ???
232+
| val t: String = (s q (s, s)).nn""".stripMargin,
233+
ctxx = ctxx
234+
)
235+
236+
@Test def addNN4 =
237+
val ctxx = newContext
238+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
239+
checkCodeAction(
240+
code =
241+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
242+
| def q(s2: String, s3: String): String | Null = null
243+
|}
244+
| val s: String = ???
245+
| val t: String = s.q(s, s)""".stripMargin,
246+
title = "Add .nn",
247+
expected =
248+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
249+
| def q(s2: String, s3: String): String | Null = null
250+
|}
251+
| val s: String = ???
252+
| val t: String = s.q(s, s).nn""".stripMargin,
253+
ctxx = ctxx
254+
)
255+
256+
@Test def addNN5 =
257+
val ctxx = newContext
258+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
259+
checkCodeAction(
260+
code =
261+
"""val s: String | Null = ???
262+
|val t: String = s match {
263+
| case _: String => "foo"
264+
| case _ => s
265+
|}""".stripMargin,
266+
title = "Add .nn",
267+
expected =
268+
"""val s: String | Null = ???
269+
|val t: String = s match {
270+
| case _: String => "foo"
271+
| case _ => s.nn
272+
|}""".stripMargin,
273+
ctxx = ctxx
274+
)
275+
276+
@Test def addNN6 =
277+
val ctxx = newContext
278+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
279+
checkCodeAction(
280+
code =
281+
"""val s: String | Null = ???
282+
|val t: String = if (s != null) "foo" else s""".stripMargin,
283+
title = "Add .nn",
284+
expected =
285+
"""val s: String | Null = ???
286+
|val t: String = if (s != null) "foo" else s.nn""".stripMargin,
287+
ctxx = ctxx
288+
)
289+
290+
@Test def addNN7 =
291+
val ctxx = newContext
292+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
293+
checkCodeAction(
294+
code =
295+
"""given ctx: String | Null = null
296+
|def f(using c: String): String = c
297+
|val s: String = f(using ctx)""".stripMargin,
298+
title = "Add .nn",
299+
expected =
300+
"""given ctx: String | Null = null
301+
|def f(using c: String): String = c
302+
|val s: String = f(using ctx.nn)""".stripMargin,
303+
ctxx = ctxx
304+
)
305+
182306
// Make sure we're not using the default reporter, which is the ConsoleReporter,
183307
// meaning they will get reported in the test run and that's it.
184308
private def newContext =

0 commit comments

Comments
 (0)