Skip to content

Commit 3df1473

Browse files
authored
Do not discard amended format when f-interpolator warns (#23697)
Also use the same error of kind `Interpolation` for warnings in `s`. There are assorted errors that `f` emits, and a handful of warnings (about format string edge cases). Previously, `f` would discard the amended "parts" and adapted "args" on any diagnostic; the transform would just call format directly, so the output looks normal. But the `toString` warning revealed that it loses the "injected" `%s` specifier; that would be witnessed only if one of the rare "edge case" warnings happened to be emitted. Now it does not discard on warnings, only on hard errors. Fixes #23693
2 parents be93510 + f73185f commit 3df1473

File tree

6 files changed

+102
-46
lines changed

6 files changed

+102
-46
lines changed

compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
116116
case Nil =>
117117

118118
loop(parts, n = 0)
119-
if reported then (Nil, Nil)
119+
if reported then (Nil, Nil) // on error, Transform.checked will revert to unamended inputs
120120
else
121121
assert(argc == actuals.size, s"Expected ${argc} args but got ${actuals.size} for [${parts.mkString(", ")}]")
122122
(amended.toList, actuals.toList)
@@ -320,5 +320,4 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
320320
.tap(_ => reported = true)
321321
def partWarning(message: String, index: Int, offset: Int, end: Int): Unit =
322322
r.warning(BadFormatInterpolation(message), partPosAt(index, offset, end))
323-
.tap(_ => reported = true)
324323
end TypedFormatChecker

compiler/src/dotty/tools/dotc/transform/localopt/FormatInterpolatorTransform.scala

Lines changed: 0 additions & 39 deletions
This file was deleted.

compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import dotty.tools.dotc.core.Contexts.*
1010
import dotty.tools.dotc.core.StdNames.*
1111
import dotty.tools.dotc.core.Symbols.*
1212
import dotty.tools.dotc.core.Types.*
13+
import dotty.tools.dotc.printing.Formatting.*
14+
import dotty.tools.dotc.reporting.BadFormatInterpolation
1315
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
1416
import dotty.tools.dotc.typer.ConstFold
1517

@@ -22,16 +24,17 @@ import dotty.tools.dotc.typer.ConstFold
2224
*/
2325
class StringInterpolatorOpt extends MiniPhase:
2426
import tpd.*
27+
import StringInterpolatorOpt.*
2528

26-
override def phaseName: String = StringInterpolatorOpt.name
29+
override def phaseName: String = name
2730

2831
override def description: String = StringInterpolatorOpt.description
2932

3033
override def checkPostCondition(tree: tpd.Tree)(using Context): Unit =
3134
tree match
3235
case tree: RefTree =>
3336
val sym = tree.symbol
34-
assert(!StringInterpolatorOpt.isCompilerIntrinsic(sym),
37+
assert(!isCompilerIntrinsic(sym),
3538
i"$tree in ${ctx.owner.showLocated} should have been rewritten by phase $phaseName")
3639
case _ =>
3740

@@ -117,10 +120,10 @@ class StringInterpolatorOpt extends MiniPhase:
117120
!(tp =:= defn.StringType)
118121
&& {
119122
tp =:= defn.UnitType
120-
&& { report.warning("interpolated Unit value", t.srcPos); true }
123+
&& { report.warning(bfi"interpolated Unit value", t.srcPos); true }
121124
||
122125
!tp.isPrimitiveValueType
123-
&& { report.warning("interpolation uses toString", t.srcPos); true }
126+
&& { report.warning(bfi"interpolation uses toString", t.srcPos); true }
124127
}
125128
if ctx.settings.Whas.toStringInterpolated then
126129
checkIsStringify(t.tpe): Unit
@@ -134,10 +137,38 @@ class StringInterpolatorOpt extends MiniPhase:
134137
case _ => false
135138
// Perform format checking and normalization, then make it StringOps(fmt).format(args1) with tweaked args
136139
def transformF(fun: Tree, args: Tree): Tree =
137-
val (fmt, args1) = FormatInterpolatorTransform.checked(fun, args)
140+
// For f"${arg}%xpart", check format conversions and return (format, args) for String.format(format, args).
141+
def checked(args0: Tree)(using Context): (Tree, Tree) =
142+
val (partsExpr, parts) = fun match
143+
case TypeApply(Select(Apply(_, (parts: SeqLiteral) :: Nil), _), _) =>
144+
(parts.elems, parts.elems.map { case Literal(Constant(s: String)) => s })
145+
case _ =>
146+
report.error("Expected statically known StringContext", fun.srcPos)
147+
(Nil, Nil)
148+
val (args, elemtpt) = args0 match
149+
case seqlit: SeqLiteral => (seqlit.elems, seqlit.elemtpt)
150+
case _ =>
151+
report.error("Expected statically known argument list", args0.srcPos)
152+
(Nil, EmptyTree)
153+
154+
def literally(s: String) = Literal(Constant(s))
155+
if parts.lengthIs != args.length + 1 then
156+
val badParts =
157+
if parts.isEmpty then "there are no parts"
158+
else s"too ${if parts.lengthIs > args.length + 1 then "few" else "many"} arguments for interpolated string"
159+
report.error(badParts, fun.srcPos)
160+
(literally(""), args0)
161+
else
162+
val checker = TypedFormatChecker(partsExpr, parts, args)
163+
val (format, formatArgs) = checker.checked
164+
if format.isEmpty then (literally(parts.mkString), args0) // on error just use unchecked inputs
165+
else (literally(format.mkString), SeqLiteral(formatArgs.toList, elemtpt))
166+
end checked
167+
val (fmt, args1) = checked(args)
138168
resolveConstructor(defn.StringOps.typeRef, List(fmt))
139169
.select(nme.format)
140170
.appliedTo(args1)
171+
end transformF
141172
// Starting with Scala 2.13, s and raw are macros in the standard
142173
// library, so we need to expand them manually.
143174
// sc.s(args) --> standardInterpolator(processEscapes, args, sc.parts)
@@ -186,3 +217,7 @@ object StringInterpolatorOpt:
186217
sym == defn.StringContext_s ||
187218
sym == defn.StringContext_f ||
188219
sym == defn.StringContext_raw
220+
221+
extension (sc: StringContext)
222+
def bfi(args: Shown*)(using Context): BadFormatInterpolation =
223+
BadFormatInterpolation(i(sc)(args*))

tests/run/i23693.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//> using options -Wtostring-interpolated
2+
3+
// verify ~warning messages and~ runtime result
4+
// never mind, the test rig doesn't log diagnostics! unlike beloved partest.
5+
6+
// Sadly, junit is not available.
7+
//import org.junit.Assert.assertEquals as jassert
8+
9+
def assertEquals(expected: String)(actual: String): Unit = assert(expected == actual)
10+
11+
case class K(i: Int)
12+
13+
@main def Test =
14+
val k = K(42)
15+
assertEquals("k == K(42)"):
16+
s"k == $k"
17+
assertEquals("\\k == \\K(42)"):
18+
raw"\k == \$k"
19+
assertEquals("k == K(42)"):
20+
f"k == $k"
21+
assertEquals("k == K(42)"):
22+
f"k == $k%s"

tests/warn/i23693.check

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- [E209] Interpolation Warning: tests/warn/i23693.scala:11:12 ---------------------------------------------------------
2+
11 | s"k == $k" // warn
3+
| ^
4+
| interpolation uses toString
5+
-- [E209] Interpolation Warning: tests/warn/i23693.scala:13:16 ---------------------------------------------------------
6+
13 | raw"\k == \$k" // warn
7+
| ^
8+
| interpolation uses toString
9+
-- [E209] Interpolation Warning: tests/warn/i23693.scala:15:12 ---------------------------------------------------------
10+
15 | f"k == $k" // warn
11+
| ^
12+
| interpolation uses toString
13+
-- [E209] Interpolation Warning: tests/warn/i23693.scala:17:14 ---------------------------------------------------------
14+
17 | f"k == $k%s" // warn
15+
| ^
16+
| interpolation uses toString
17+
-- [E209] Interpolation Warning: tests/warn/i23693.scala:19:18 ---------------------------------------------------------
18+
19 | s"show == ${k.show}" // warn
19+
| ^^^^^^
20+
| interpolated Unit value

tests/warn/i23693.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//> using options -Wtostring-interpolated
2+
3+
// verify warning messages; cf run test; we must verify runtime while warning.
4+
5+
case class K(i: Int):
6+
def show: Unit = ()
7+
8+
@main def Test =
9+
val k = K(42)
10+
println:
11+
s"k == $k" // warn
12+
println:
13+
raw"\k == \$k" // warn
14+
println:
15+
f"k == $k" // warn
16+
println:
17+
f"k == $k%s" // warn
18+
println:
19+
s"show == ${k.show}" // warn

0 commit comments

Comments
 (0)