@@ -5,8 +5,6 @@ import scala.annotation.tailrec
5
5
import scala .collection .mutable .ListBuffer
6
6
import scala .util .matching .Regex .Match
7
7
8
- import PartialFunction .cond
9
-
10
8
import dotty .tools .dotc .ast .tpd .{Match => _ , * }
11
9
import dotty .tools .dotc .core .Contexts .*
12
10
import dotty .tools .dotc .core .Symbols .*
@@ -30,8 +28,9 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
30
28
def argType (argi : Int , types : Type * ): Type =
31
29
require(argi < argc, s " $argi out of range picking from $types" )
32
30
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)))
35
34
.getOrElse {
36
35
report.argError(s " Found: ${tpe.show}, Required: ${types.map(_.show).mkString(" , " )}" , argi)
37
36
actuals += args(argi)
@@ -64,50 +63,57 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
64
63
65
64
/** For N part strings and N-1 args to interpolate, normalize parts and check arg types.
66
65
*
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.
68
67
*/
69
68
def checked : (List [String ], List [Tree ]) =
70
69
val amended = ListBuffer .empty[String ]
71
70
val convert = ListBuffer .empty[Conversion ]
72
71
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
+
73
109
@ 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 =>
111
117
112
118
loop(parts, n = 0 )
113
119
if reported then (Nil , Nil )
@@ -125,10 +131,8 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
125
131
def intOf (g : SpecGroup ): Option [Int ] = group(g).map(_.toInt)
126
132
127
133
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 }
132
136
133
137
enum Kind :
134
138
case StringXn , HashXn , BooleanXn , CharacterXn , IntegralXn , FloatingPointXn , DateTimeXn , LiteralXn , ErrorXn
@@ -147,9 +151,10 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
147
151
// the conversion char is the head of the op string (but see DateTimeXn)
148
152
val cc : Char =
149
153
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 )
153
158
154
159
def isIndexed : Boolean = index.nonEmpty || hasFlag('<' )
155
160
def isError : Boolean = kind == ErrorXn
@@ -209,18 +214,28 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List
209
214
// is the specifier OK with the given arg
210
215
def accepts (arg : Type ): Boolean =
211
216
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
217
227
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" )
218
233
219
234
// what arg type if any does the conversion accept
220
235
def acceptableVariants : List [Type ] =
221
236
kind match
222
237
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
224
239
case HashXn => defn.AnyType :: Nil
225
240
case CharacterXn => defn.CharType :: defn.ByteType :: defn.ShortType :: defn.IntType :: Nil
226
241
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
249
264
250
265
object Conversion :
251
266
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
267
267
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" ))
270
288
end apply
289
+ // construct a default %s conversion
290
+ def stringXn (i : Int ): Conversion = new Conversion (formatPattern.findAllMatchIn(" %" ).next(), i, StringXn )
271
291
val literalHelp = " use %% for literal %, %n for newline"
272
292
end Conversion
273
293
0 commit comments