Skip to content

Commit f1003a4

Browse files
authored
Merge pull request #574 from scala/backport-lts-3.3-23598
Backport "Add quick fix to add .nn" to 3.3 LTS
2 parents d750b47 + 33f5264 commit f1003a4

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-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
@@ -53,6 +53,8 @@ object desugar {
5353
*/
5454
val ForArtifact: Property.Key[Unit] = Property.StickyKey()
5555

56+
val WasTypedInfix: Property.Key[Unit] = Property.StickyKey()
57+
5658
/** What static check should be applied to a Match? */
5759
enum MatchCheck {
5860
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -1369,10 +1371,12 @@ object desugar {
13691371
case _ =>
13701372
Apply(sel, arg :: Nil)
13711373

1372-
if op.name.isRightAssocOperatorName then
1374+
val apply = if op.name.isRightAssocOperatorName then
13731375
makeOp(right, left, Span(op.span.start, right.span.end))
13741376
else
13751377
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
1378+
apply.pushAttachment(WasTypedInfix, ())
1379+
return apply
13761380
}
13771381

13781382
/** 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, ScalaVersion}
1819
import transform.patmat.Space
1920
import transform.patmat.SpaceEngine
@@ -295,6 +296,11 @@ extends NotFoundMsg(MissingIdentID) {
295296
class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tree], addenda: => String*)(using Context)
296297
extends TypeMismatchMsg(found, expected)(TypeMismatchID):
297298

299+
private val shouldSuggestNN =
300+
if ctx.mode.is(Mode.SafeNulls) && expected.isValueType then
301+
found frozen_<:< OrNull(expected)
302+
else false
303+
298304
def msg(using Context) =
299305
// replace constrained TypeParamRefs and their typevars by their bounds where possible
300306
// and the bounds are not f-bounds.
@@ -355,6 +361,26 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre
355361
val treeStr = inTree.map(x => s"\nTree:\n\n${x.show}\n").getOrElse("")
356362
treeStr + "\n" + super.explain
357363

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

360386
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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,159 @@ class CodeActionTest extends DottyTest:
136136
afterPhase = "patternMatcher"
137137
)
138138

139+
@Test def removeNN =
140+
val ctxx = newContext
141+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
142+
checkCodeAction(
143+
code =
144+
"""|val s: String|Null = "foo".nn
145+
|""".stripMargin,
146+
title = "Remove unnecessary .nn",
147+
expected =
148+
"""|val s: String|Null = "foo"
149+
|""".stripMargin,
150+
ctxx = ctxx
151+
)
152+
153+
154+
@Test def removeNN2 =
155+
val ctxx = newContext
156+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
157+
checkCodeAction(
158+
code =
159+
"""val s: String|Null = null.nn
160+
|""".stripMargin,
161+
title = "Remove unnecessary .nn",
162+
expected =
163+
"""val s: String|Null = null
164+
|""".stripMargin,
165+
ctxx = ctxx
166+
)
167+
168+
@Test def addNN1 =
169+
val ctxx = newContext
170+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
171+
checkCodeAction(
172+
code =
173+
"""val s: String|Null = ???
174+
| val t: String = s""".stripMargin,
175+
title = "Add .nn",
176+
expected =
177+
"""val s: String|Null = ???
178+
| val t: String = s.nn""".stripMargin,
179+
ctxx = ctxx
180+
)
181+
182+
@Test def addNN2 =
183+
val ctxx = newContext
184+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
185+
checkCodeAction(
186+
code =
187+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
188+
| def q(s2: String): String | Null = null
189+
|}
190+
| val s: String = ???
191+
| val t: String = s q s""".stripMargin,
192+
title = "Add .nn",
193+
expected =
194+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
195+
| def q(s2: String): String | Null = null
196+
|}
197+
| val s: String = ???
198+
| val t: String = (s q s).nn""".stripMargin,
199+
ctxx = ctxx
200+
)
201+
202+
@Test def addNN3 =
203+
val ctxx = newContext
204+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
205+
checkCodeAction(
206+
code =
207+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
208+
| def q(s2: String, s3: String): String | Null = null
209+
|}
210+
| val s: String = ???
211+
| val t: String = s q (s, s)""".stripMargin,
212+
title = "Add .nn",
213+
expected =
214+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
215+
| def q(s2: String, s3: String): String | Null = null
216+
|}
217+
| val s: String = ???
218+
| val t: String = (s q (s, s)).nn""".stripMargin,
219+
ctxx = ctxx
220+
)
221+
222+
@Test def addNN4 =
223+
val ctxx = newContext
224+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
225+
checkCodeAction(
226+
code =
227+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
228+
| def q(s2: String, s3: String): String | Null = null
229+
|}
230+
| val s: String = ???
231+
| val t: String = s.q(s, s)""".stripMargin,
232+
title = "Add .nn",
233+
expected =
234+
"""implicit class infixOpTest(val s1: String) extends AnyVal {
235+
| def q(s2: String, s3: String): String | Null = null
236+
|}
237+
| val s: String = ???
238+
| val t: String = s.q(s, s).nn""".stripMargin,
239+
ctxx = ctxx
240+
)
241+
242+
@Test def addNN5 =
243+
val ctxx = newContext
244+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
245+
checkCodeAction(
246+
code =
247+
"""val s: String | Null = ???
248+
|val t: String = s match {
249+
| case _: String => "foo"
250+
| case _ => s
251+
|}""".stripMargin,
252+
title = "Add .nn",
253+
expected =
254+
"""val s: String | Null = ???
255+
|val t: String = s match {
256+
| case _: String => "foo"
257+
| case _ => s.nn
258+
|}""".stripMargin,
259+
ctxx = ctxx
260+
)
261+
262+
@Test def addNN6 =
263+
val ctxx = newContext
264+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
265+
checkCodeAction(
266+
code =
267+
"""val s: String | Null = ???
268+
|val t: String = if (s != null) "foo" else s""".stripMargin,
269+
title = "Add .nn",
270+
expected =
271+
"""val s: String | Null = ???
272+
|val t: String = if (s != null) "foo" else s.nn""".stripMargin,
273+
ctxx = ctxx
274+
)
275+
276+
@Test def addNN7 =
277+
val ctxx = newContext
278+
ctxx.setSetting(ctxx.settings.YexplicitNulls, true)
279+
checkCodeAction(
280+
code =
281+
"""given ctx: String | Null = null
282+
|def f(using c: String): String = c
283+
|val s: String = f(using ctx)""".stripMargin,
284+
title = "Add .nn",
285+
expected =
286+
"""given ctx: String | Null = null
287+
|def f(using c: String): String = c
288+
|val s: String = f(using ctx.nn)""".stripMargin,
289+
ctxx = ctxx
290+
)
291+
139292
// Make sure we're not using the default reporter, which is the ConsoleReporter,
140293
// meaning they will get reported in the test run and that's it.
141294
private def newContext =

0 commit comments

Comments
 (0)