Skip to content

Commit 1bda625

Browse files
author
som-snytt
authored
Merge pull request scala#10776 from som-snytt/topic/warn-toString-interpolated
Warn if interpolator uses toString
2 parents 88cfc02 + 1ac2451 commit 1bda625

File tree

8 files changed

+135
-27
lines changed

8 files changed

+135
-27
lines changed

src/compiler/scala/tools/nsc/Reporting.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ object Reporting {
572572
WFlagNumericWiden,
573573
WFlagSelfImplicit,
574574
WFlagUnnamedBooleanLiteral,
575+
WFlagTostringInterpolated,
575576
WFlagValueDiscard
576577
= wflag()
577578

src/compiler/scala/tools/nsc/settings/Warnings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ trait Warnings {
123123
val warnOctalLiteral = BooleanSetting("-Woctal-literal", "Warn on obsolete octal syntax.") withAbbreviation "-Ywarn-octal-literal"
124124
val warnUnnamedBoolean = BooleanSetting("-Wunnamed-boolean-literal", "Warn about unnamed boolean literals if there is more than one or defaults are used, unless parameter has @deprecatedName.")
125125
val warnUnnamedStrict = BooleanSetting("-Wunnamed-boolean-literal-strict", "Warn about all unnamed boolean literals, unless parameter has @deprecatedName or the method has a single leading boolean parameter.").enabling(warnUnnamedBoolean :: Nil)
126+
val warnToString = BooleanSetting("-Wtostring-interpolated", "Warn when a standard interpolator uses toString.")
126127

127128
object PerformanceWarnings extends MultiChoiceEnumeration {
128129
val Captured = Choice("captured", "Modification of var in closure causes boxing.")

src/compiler/scala/tools/reflect/FastStringInterpolator.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
package scala.tools
1414
package reflect
1515

16-
import nsc.Reporting.WarningCategory.Scala3Migration
16+
import nsc.Reporting.WarningCategory.{Scala3Migration, WFlagTostringInterpolated}
1717

1818
trait FastStringInterpolator extends FormatInterpolator {
1919
import c.universe._
@@ -101,11 +101,15 @@ trait FastStringInterpolator extends FormatInterpolator {
101101
val treatedContents = lit.asInstanceOf[Literal].value.stringValue
102102
val emptyLit = treatedContents.isEmpty
103103
if (i < numLits - 1) {
104-
concatArgs += argsIndexed(i)
105-
if (!emptyLit) concatArgs += lit
106-
} else if (!emptyLit) {
107-
concatArgs += lit
104+
val arg = argsIndexed(i)
105+
if (linting && !(arg.tpe =:= definitions.StringTpe))
106+
if (arg.tpe.typeSymbol eq definitions.UnitClass)
107+
runReporting.warning(arg.pos, "interpolated Unit value", WFlagTostringInterpolated, c.internal.enclosingOwner)
108+
else if (!definitions.isPrimitiveValueType(arg.tpe))
109+
runReporting.warning(arg.pos, "interpolation uses toString", WFlagTostringInterpolated, c.internal.enclosingOwner)
110+
concatArgs += arg
108111
}
112+
if (!emptyLit) concatArgs += lit
109113
}
110114
def mkConcat(pos: Position, lhs: Tree, rhs: Tree): Tree =
111115
atPos(pos)(gen.mkMethodCall(gen.mkAttributedSelect(lhs, definitions.String_+), rhs :: Nil)).setType(definitions.StringTpe)

src/compiler/scala/tools/reflect/FormatInterpolator.scala

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
package scala.tools.reflect
1414

15-
import scala.reflect.macros.runtime.Context
16-
import scala.collection.mutable.ListBuffer
1715
import scala.PartialFunction.cond
18-
import scala.util.chaining._
16+
import scala.collection.mutable.ListBuffer
17+
import scala.reflect.macros.runtime.Context
18+
import scala.tools.nsc.Reporting.WarningCategory, WarningCategory.WFlagTostringInterpolated
1919
import scala.util.matching.Regex.Match
20+
import scala.util.chaining._
2021

2122
import java.util.Formattable
2223

@@ -31,6 +32,14 @@ abstract class FormatInterpolator {
3132
import definitions._
3233
import treeInfo.Applied
3334

35+
protected var linting = settings.warnToString.value
36+
37+
protected final def withoutLinting[A](body: => A): A = {
38+
val linted = linting
39+
linting = false
40+
try body finally linting = linted
41+
}
42+
3443
private def bail(msg: String) = global.abort(msg)
3544

3645
def concatenate(parts: List[Tree], args: List[Tree]): Tree
@@ -87,8 +96,9 @@ abstract class FormatInterpolator {
8796

8897
def argType(argi: Int, types: Type*): Type = {
8998
val tpe = argTypes(argi)
90-
types.find(t => argConformsTo(argi, tpe, t))
91-
.orElse(types.find(t => argConvertsTo(argi, tpe, t)))
99+
types.find(t => t != AnyTpe && argConformsTo(argi, tpe, t))
100+
.orElse(types.find(t => t != AnyTpe && argConvertsTo(argi, tpe, t)))
101+
.orElse(types.find(t => t == AnyTpe && argConformsTo(argi, tpe, t)))
92102
.getOrElse {
93103
val msg = "type mismatch" + {
94104
val req = raw"required: (.*)".r.unanchored
@@ -120,7 +130,7 @@ abstract class FormatInterpolator {
120130
// Check the % fields in this part.
121131
def loop(remaining: List[Tree], n: Int): Unit =
122132
remaining match {
123-
case part0 :: more =>
133+
case part0 :: remaining =>
124134
val part1 = part0 match {
125135
case Literal(Constant(x: String)) => x
126136
case _ => throw new IllegalArgumentException("internal error: argument parts must be a list of string literals")
@@ -130,14 +140,17 @@ abstract class FormatInterpolator {
130140

131141
def insertStringConversion(): Unit = {
132142
amended += "%s" + part
133-
convert += Conversion(formatPattern.findAllMatchIn("%s").next(), part0.pos, argc) // improve
134-
argType(n-1, AnyTpe)
143+
val cv = Conversion(part0.pos, argc)
144+
cv.accepts(argType(n-1, AnyTpe))
145+
convert += cv
146+
cv.lintToString(argTypes(n-1))
135147
}
136148
def errorLeading(op: Conversion) = op.errorAt(Spec)(s"conversions must follow a splice; ${Conversion.literalHelp}")
137149
def accept(op: Conversion): Unit = {
138150
if (!op.isLeading) errorLeading(op)
139151
op.accepts(argType(n-1, op.acceptableVariants: _*))
140152
amended += part
153+
op.lintToString(argTypes(n-1))
141154
}
142155

143156
if (n == 0) amended += part
@@ -164,7 +177,7 @@ abstract class FormatInterpolator {
164177
else if (!cv.isLiteral && !cv.isIndexed) errorLeading(cv)
165178
formatting = true
166179
}
167-
loop(more, n = n + 1)
180+
loop(remaining, n = n + 1)
168181
case Nil =>
169182
}
170183
loop(parts, n = 0)
@@ -178,7 +191,11 @@ abstract class FormatInterpolator {
178191
val format = amended.mkString
179192
if (actuals.isEmpty && !formatting) constantly(format)
180193
else if (!reported && actuals.forall(treeInfo.isLiteralString)) constantly(format.format(actuals.map(_.asInstanceOf[Literal].value.value).toIndexedSeq: _*))
181-
else if (!formatting) concatenate(amended.map(p => constantly(p.stripPrefix("%s"))).toList, actuals.toList)
194+
else if (!formatting) {
195+
withoutLinting { // already warned
196+
concatenate(amended.map(p => constantly(p.stripPrefix("%s"))).toList, actuals.toList)
197+
}
198+
}
182199
else {
183200
val scalaPackage = Select(Ident(nme.ROOTPKG), TermName("scala"))
184201
val newStringOps = Select(
@@ -218,6 +235,7 @@ abstract class FormatInterpolator {
218235
case ErrorXn => op(0)
219236
case DateTimeXn if op.length > 1 => op(1)
220237
case DateTimeXn => '?'
238+
case StringXn if op.isEmpty => 's' // accommodate the default %s
221239
case _ => op(0)
222240
}
223241

@@ -295,13 +313,19 @@ abstract class FormatInterpolator {
295313
}
296314
case _ => true
297315
}
316+
def lintToString(arg: Type): Unit =
317+
if (linting && kind == StringXn && !(arg =:= StringTpe))
318+
if (arg.typeSymbol eq UnitClass)
319+
warningAt(CC)("interpolated Unit value", WFlagTostringInterpolated)
320+
else if (!definitions.isPrimitiveValueType(arg))
321+
warningAt(CC)("interpolation uses toString", WFlagTostringInterpolated)
298322

299323
// what arg type if any does the conversion accept
300324
def acceptableVariants: List[Type] =
301325
kind match {
302326
case StringXn if hasFlag('#') => FormattableTpe :: Nil
303327
case StringXn => AnyTpe :: Nil
304-
case BooleanXn => BooleanTpe :: NullTpe :: Nil
328+
case BooleanXn => BooleanTpe :: NullTpe :: AnyTpe :: Nil // warn if not boolean
305329
case HashXn => AnyTpe :: Nil
306330
case CharacterXn => CharTpe :: ByteTpe :: ShortTpe :: IntTpe :: Nil
307331
case IntegralXn => IntTpe :: LongTpe :: ByteTpe :: ShortTpe :: BigIntTpe :: Nil
@@ -331,7 +355,7 @@ abstract class FormatInterpolator {
331355

332356
def groupPosAt(g: SpecGroup, i: Int) = pos.withPoint(pos.point + descriptor.offset(g, i))
333357
def errorAt(g: SpecGroup, i: Int = 0)(msg: String) = c.error(groupPosAt(g, i), msg).tap(_ => reported = true)
334-
def warningAt(g: SpecGroup, i: Int = 0)(msg: String) = c.warning(groupPosAt(g, i), msg)
358+
def warningAt(g: SpecGroup, i: Int = 0)(msg: String, cat: WarningCategory = WarningCategory.Other) = c.callsiteTyper.context.warning(groupPosAt(g, i), msg, cat, Nil)
335359
}
336360

337361
object Conversion {
@@ -356,6 +380,9 @@ abstract class FormatInterpolator {
356380
case None => new Conversion(m, p, ErrorXn, argc).tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp"))
357381
}
358382
}
383+
// construct a default %s conversion
384+
def apply(p: Position, argc: Int): Conversion =
385+
new Conversion(formatPattern.findAllMatchIn("%").next(), p, StringXn, argc)
359386
val literalHelp = "use %% for literal %, %n for newline"
360387
}
361388

test/files/neg/stringinterpolation_macro-neg.check

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ stringinterpolation_macro-neg.scala:15: error: too many arguments for interpolat
1010
stringinterpolation_macro-neg.scala:16: error: too few arguments for interpolated string
1111
new StringContext("", "").f()
1212
^
13-
stringinterpolation_macro-neg.scala:19: error: type mismatch;
14-
found : String
15-
required: Boolean, Null
16-
f"$s%b"
17-
^
1813
stringinterpolation_macro-neg.scala:20: error: type mismatch;
1914
found : String
2015
required: Char, Byte, Short, Int
@@ -162,6 +157,9 @@ stringinterpolation_macro-neg.scala:77: error: Missing conversion operator in '%
162157
stringinterpolation_macro-neg.scala:80: error: conversions must follow a splice; use %% for literal %, %n for newline
163158
f"${d}random-leading-junk%d"
164159
^
160+
stringinterpolation_macro-neg.scala:19: warning: Boolean format is null test for non-Boolean
161+
f"$s%b"
162+
^
165163
stringinterpolation_macro-neg.scala:63: warning: Index is not this arg
166164
f"${8}%d ${9}%1$$d"
167165
^
@@ -171,5 +169,5 @@ stringinterpolation_macro-neg.scala:64: warning: Argument index ignored if '<' f
171169
stringinterpolation_macro-neg.scala:65: warning: Index is not this arg
172170
f"$s%s $s%1$$s"
173171
^
174-
3 warnings
175-
46 errors
172+
4 warnings
173+
45 errors
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
tostring-interpolated.scala:7: error: interpolation uses toString
2+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.f
3+
def f = f"$c" // warn
4+
^
5+
tostring-interpolated.scala:8: error: interpolation uses toString
6+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.s
7+
def s = s"$c" // warn
8+
^
9+
tostring-interpolated.scala:9: error: interpolation uses toString
10+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.r
11+
def r = raw"$c" // warn
12+
^
13+
tostring-interpolated.scala:11: error: interpolation uses toString
14+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.format
15+
def format = f"${c.x}%d in $c or $c%s" // warn using c.toString // warn
16+
^
17+
tostring-interpolated.scala:11: error: interpolation uses toString
18+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.format
19+
def format = f"${c.x}%d in $c or $c%s" // warn using c.toString // warn
20+
^
21+
tostring-interpolated.scala:13: warning: Boolean format is null test for non-Boolean
22+
def bool = f"$c%b" // warn just a null check
23+
^
24+
tostring-interpolated.scala:15: error: interpolation uses toString
25+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.oops
26+
def oops = s"${null} slipped thru my fingers" // warn
27+
^
28+
tostring-interpolated.scala:20: error: interpolation uses toString
29+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=T.greeting
30+
def greeting = s"$sb, world" // warn
31+
^
32+
tostring-interpolated.scala:31: error: interpolated Unit value
33+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=Mitigations.unitized
34+
def unitized = s"unfortunately $shown" // warn accidental unit value
35+
^
36+
tostring-interpolated.scala:32: error: interpolated Unit value
37+
Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=w-flag-tostring-interpolated, site=Mitigations.funitized
38+
def funitized = f"unfortunately $shown" // warn accidental unit value
39+
^
40+
1 warning
41+
9 errors
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//> using options -Wconf:cat=w-flag-tostring-interpolated:e -Wtostring-interpolated
2+
3+
case class C(x: Int)
4+
5+
trait T {
6+
def c = C(42)
7+
def f = f"$c" // warn
8+
def s = s"$c" // warn
9+
def r = raw"$c" // warn
10+
11+
def format = f"${c.x}%d in $c or $c%s" // warn using c.toString // warn
12+
13+
def bool = f"$c%b" // warn just a null check
14+
15+
def oops = s"${null} slipped thru my fingers" // warn
16+
17+
def ok = s"${c.toString}"
18+
19+
def sb = new StringBuilder().append("hello")
20+
def greeting = s"$sb, world" // warn
21+
}
22+
23+
class Mitigations {
24+
25+
val s = "hello, world"
26+
val i = 42
27+
def shown() = println("shown")
28+
29+
def ok = s"$s is ok"
30+
def jersey = s"number $i"
31+
def unitized = s"unfortunately $shown" // warn accidental unit value
32+
def funitized = f"unfortunately $shown" // warn accidental unit value
33+
34+
def nopct = f"$s is ok"
35+
def nofmt = f"number $i"
36+
}

test/files/run/f-interpolator-unit.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object Test extends App {
3333
final val tester = "hello"
3434
final val number = "42" // strings only, alas
3535

36-
def assertEquals(s0: String, s1: String) = assert(s0 == s1, s"$s0 == $s1")
36+
def assertEquals(s0: String, s1: String, i: Int = -1) = assert(s0 == s1, s"$s0 == $s1${if (i >= 0) " at " + i.toString else ""}")
3737

3838
def noEscape() = {
3939
val s = "string"
@@ -134,7 +134,7 @@ object Test extends App {
134134
f"${null}%b" -> "false",
135135
f"${false}%b" -> "false",
136136
f"${true}%b" -> "true",
137-
f"${true && false}%b" -> "false",
137+
f"${true && false}%b" -> "false",
138138
f"${java.lang.Boolean.valueOf(false)}%b" -> "false",
139139
f"${java.lang.Boolean.valueOf(true)}%b" -> "true",
140140

@@ -255,7 +255,7 @@ object Test extends App {
255255
f"z" -> "z"
256256
)
257257

258-
for ((f, s) <- ss) assertEquals(s, f)
258+
for (((f, s), i) <- ss.zipWithIndex) assertEquals(s, f, i)
259259
}
260260

261261
noEscape()

0 commit comments

Comments
 (0)