@@ -5,8 +5,6 @@ import scala.annotation.tailrec
55import scala .collection .mutable .ListBuffer
66import scala .util .matching .Regex .Match
77
8- import PartialFunction .cond
9-
108import dotty .tools .dotc .ast .tpd .{Match => _ , * }
119import dotty .tools .dotc .core .Contexts .*
1210import dotty .tools .dotc .core .Symbols .*
@@ -30,8 +28,9 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
3028 def argType (argi : Int , types : Type * ): Type =
3129 require(argi < argc, s " $argi out of range picking from $types" )
3230 val tpe = argTypes(argi)
33- types.find(t => argConformsTo(argi, tpe, t))
34- .orElse(types.find(t => argConvertsTo(argi, tpe, t)))
31+ types.find(t => t != defn.AnyType && argConformsTo(argi, tpe, t))
32+ .orElse(types.find(t => t != defn.AnyType && argConvertsTo(argi, tpe, t)))
33+ .orElse(types.find(t => t == defn.AnyType && argConformsTo(argi, tpe, t)))
3534 .getOrElse {
3635 report.argError(s " Found: ${tpe.show}, Required: ${types.map(_.show).mkString(" , " )}" , argi)
3736 actuals += args(argi)
@@ -64,50 +63,57 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
6463
6564 /** For N part strings and N-1 args to interpolate, normalize parts and check arg types.
6665 *
67- * Returns normalized part strings and args, where args correcpond to conversions in tail of parts.
66+ * Returns normalized part strings and args, where args correspond to conversions in tail of parts.
6867 */
6968 def checked : (List [String ], List [Tree ]) =
7069 val amended = ListBuffer .empty[String ]
7170 val convert = ListBuffer .empty[Conversion ]
7271
72+ def checkPart (part : String , n : Int ): Unit =
73+ val matches = formatPattern.findAllMatchIn(part)
74+
75+ def insertStringConversion (): Unit =
76+ amended += " %s" + part
77+ val cv = Conversion .stringXn(n)
78+ cv.accepts(argType(n- 1 , defn.AnyType ))
79+ convert += cv
80+ cv.lintToString(argTypes(n- 1 ))
81+
82+ def errorLeading (op : Conversion ) = op.errorAt(Spec ):
83+ s " conversions must follow a splice; ${Conversion .literalHelp}"
84+
85+ def accept (op : Conversion ): Unit =
86+ if ! op.isLeading then errorLeading(op)
87+ op.accepts(argType(n- 1 , op.acceptableVariants* ))
88+ amended += part
89+ convert += op
90+ op.lintToString(argTypes(n- 1 ))
91+
92+ // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed
93+ if n == 0 then amended += part
94+ else if ! matches.hasNext then insertStringConversion()
95+ else
96+ val cv = Conversion (matches.next(), n)
97+ if cv.isLiteral then insertStringConversion()
98+ else if cv.isIndexed then
99+ if cv.index.getOrElse(- 1 ) == n then accept(cv) else insertStringConversion()
100+ else if ! cv.isError then accept(cv)
101+
102+ // any remaining conversions in this part must be either literals or indexed
103+ while matches.hasNext do
104+ val cv = Conversion (matches.next(), n)
105+ if n == 0 && cv.hasFlag('<' ) then cv.badFlag('<' , " No last arg" )
106+ else if ! cv.isLiteral && ! cv.isIndexed then errorLeading(cv)
107+ end checkPart
108+
73109 @ tailrec
74- def loop (remaining : List [String ], n : Int ): Unit =
75- remaining match
76- case part0 :: more =>
77- def badPart (t : Throwable ): String = " " .tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0 ))
78- val part = try StringContext .processEscapes(part0) catch badPart
79- val matches = formatPattern.findAllMatchIn(part)
80-
81- def insertStringConversion (): Unit =
82- amended += " %s" + part
83- convert += Conversion (formatPattern.findAllMatchIn(" %s" ).next(), n) // improve
84- argType(n- 1 , defn.AnyType )
85- def errorLeading (op : Conversion ) = op.errorAt(Spec )(s " conversions must follow a splice; ${Conversion .literalHelp}" )
86- def accept (op : Conversion ): Unit =
87- if ! op.isLeading then errorLeading(op)
88- op.accepts(argType(n- 1 , op.acceptableVariants* ))
89- amended += part
90- convert += op
91-
92- // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed
93- if n == 0 then amended += part
94- else if ! matches.hasNext then insertStringConversion()
95- else
96- val cv = Conversion (matches.next(), n)
97- if cv.isLiteral then insertStringConversion()
98- else if cv.isIndexed then
99- if cv.index.getOrElse(- 1 ) == n then accept(cv) else insertStringConversion()
100- else if ! cv.isError then accept(cv)
101-
102- // any remaining conversions in this part must be either literals or indexed
103- while matches.hasNext do
104- val cv = Conversion (matches.next(), n)
105- if n == 0 && cv.hasFlag('<' ) then cv.badFlag('<' , " No last arg" )
106- else if ! cv.isLiteral && ! cv.isIndexed then errorLeading(cv)
107-
108- loop(more, n + 1 )
109- case Nil => ()
110- end loop
110+ def loop (remaining : List [String ], n : Int ): Unit = remaining match
111+ case part0 :: remaining =>
112+ def badPart (t : Throwable ): String = " " .tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0 ))
113+ val part = try StringContext .processEscapes(part0) catch badPart
114+ checkPart(part, n)
115+ loop(remaining, n + 1 )
116+ case Nil =>
111117
112118 loop(parts, n = 0 )
113119 if reported then (Nil , Nil )
@@ -125,10 +131,8 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
125131 def intOf (g : SpecGroup ): Option [Int ] = group(g).map(_.toInt)
126132
127133 extension (inline value : Boolean )
128- inline def or (inline body : => Unit ): Boolean = value || { body ; false }
129- inline def orElse (inline body : => Unit ): Boolean = value || { body ; true }
130- inline def and (inline body : => Unit ): Boolean = value && { body ; true }
131- inline def but (inline body : => Unit ): Boolean = value && { body ; false }
134+ inline infix def or (inline body : => Unit ): Boolean = value || { body; false }
135+ inline infix def and (inline body : => Unit ): Boolean = value && { body; true }
132136
133137 enum Kind :
134138 case StringXn , HashXn , BooleanXn , CharacterXn , IntegralXn , FloatingPointXn , DateTimeXn , LiteralXn , ErrorXn
@@ -147,9 +151,10 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
147151 // the conversion char is the head of the op string (but see DateTimeXn)
148152 val cc : Char =
149153 kind match
150- case ErrorXn => if op.isEmpty then '?' else op(0 )
151- case DateTimeXn => if op.length > 1 then op(1 ) else '?'
152- case _ => op(0 )
154+ case ErrorXn => if op.isEmpty then '?' else op(0 )
155+ case DateTimeXn => if op.length <= 1 then '?' else op(1 )
156+ case StringXn => if op.isEmpty then 's' else op(0 ) // accommodate the default %s
157+ case _ => op(0 )
153158
154159 def isIndexed : Boolean = index.nonEmpty || hasFlag('<' )
155160 def isError : Boolean = kind == ErrorXn
@@ -209,18 +214,28 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
209214 // is the specifier OK with the given arg
210215 def accepts (arg : Type ): Boolean =
211216 kind match
212- case BooleanXn => arg == defn.BooleanType orElse warningAt(CC )(" Boolean format is null test for non-Boolean" )
213- case IntegralXn =>
214- arg == BigIntType || ! cond(cc) {
215- case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) => " + (" .filter(hasFlag).foreach(bad => badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )) ; true
216- }
217+ case BooleanXn if arg != defn.BooleanType =>
218+ warningAt(CC ):
219+ """ non-Boolean value formats as "true" for non-null references and boxed primitives, otherwise "false""""
220+ true
221+ case IntegralXn if arg != BigIntType =>
222+ cc match
223+ case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) =>
224+ " + (" .filter(hasFlag).foreach: bad =>
225+ badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )
226+ false
217227 case _ => true
228+ case _ => true
229+
230+ def lintToString (arg : Type ): Unit =
231+ if ctx.settings.Whas .toStringInterpolated && kind == StringXn && ! (arg.widen =:= defn.StringType ) && ! arg.isPrimitiveValueType
232+ then warningAt(CC )(" interpolation uses toString" )
218233
219234 // what arg type if any does the conversion accept
220235 def acceptableVariants : List [Type ] =
221236 kind match
222237 case StringXn => if hasFlag('#' ) then FormattableType :: Nil else defn.AnyType :: Nil
223- case BooleanXn => defn.BooleanType :: defn.NullType :: Nil
238+ case BooleanXn => defn.BooleanType :: defn.NullType :: defn. AnyType :: Nil // warn if not boolean
224239 case HashXn => defn.AnyType :: Nil
225240 case CharacterXn => defn.CharType :: defn.ByteType :: defn.ShortType :: defn.IntType :: Nil
226241 case IntegralXn => defn.IntType :: defn.LongType :: defn.ByteType :: defn.ShortType :: BigIntType :: Nil
@@ -249,25 +264,30 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
249264
250265 object Conversion :
251266 def apply (m : Match , i : Int ): Conversion =
252- def kindOf (cc : Char ) = cc match
253- case 's' | 'S' => StringXn
254- case 'h' | 'H' => HashXn
255- case 'b' | 'B' => BooleanXn
256- case 'c' | 'C' => CharacterXn
257- case 'd' | 'o' |
258- 'x' | 'X' => IntegralXn
259- case 'e' | 'E' |
260- 'f' |
261- 'g' | 'G' |
262- 'a' | 'A' => FloatingPointXn
263- case 't' | 'T' => DateTimeXn
264- case '%' | 'n' => LiteralXn
265- case _ => ErrorXn
266- end kindOf
267267 m.group(CC ) match
268- case Some (cc) => new Conversion (m, i, kindOf(cc(0 ))).tap(_.verify)
269- case None => new Conversion (m, i, ErrorXn ).tap(_.errorAt(Spec )(s " Missing conversion operator in ' ${m.matched}'; $literalHelp" ))
268+ case Some (cc) =>
269+ val xn = cc(0 ) match
270+ case 's' | 'S' => StringXn
271+ case 'h' | 'H' => HashXn
272+ case 'b' | 'B' => BooleanXn
273+ case 'c' | 'C' => CharacterXn
274+ case 'd' | 'o' |
275+ 'x' | 'X' => IntegralXn
276+ case 'e' | 'E' |
277+ 'f' |
278+ 'g' | 'G' |
279+ 'a' | 'A' => FloatingPointXn
280+ case 't' | 'T' => DateTimeXn
281+ case '%' | 'n' => LiteralXn
282+ case _ => ErrorXn
283+ new Conversion (m, i, xn)
284+ .tap(_.verify)
285+ case None =>
286+ new Conversion (m, i, ErrorXn )
287+ .tap(_.errorAt(Spec )(s " Missing conversion operator in ' ${m.matched}'; $literalHelp" ))
270288 end apply
289+ // construct a default %s conversion
290+ def stringXn (i : Int ): Conversion = new Conversion (formatPattern.findAllMatchIn(" %" ).next(), i, StringXn )
271291 val literalHelp = " use %% for literal %, %n for newline"
272292 end Conversion
273293
0 commit comments