@@ -9,6 +9,8 @@ import scala.util.matching.Regex.Match
99
1010import java .util .{Calendar , Date , Formattable }
1111
12+ import PartialFunction .cond
13+
1214/** Formatter string checker. */
1315abstract class FormatChecker (using reporter : InterpolationReporter ):
1416
@@ -19,7 +21,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
1921 // count of args, for checking indexes
2022 def argc : Int
2123
22- val allFlags = " -#+ 0,(< "
24+ // match a conversion specifier
2325 val formatPattern = """ %(?:(\d+)\$)?([-#+ 0,(<]+)?(\d+)?(\.\d+)?([tT]?[%a-zA-Z])?""" .r
2426
2527 // ordinal is the regex group index in the format pattern
@@ -95,18 +97,16 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
9597 extension (inline value : Boolean )
9698 inline def or (inline body : => Unit ): Boolean = value || { body ; false }
9799 inline def orElse (inline body : => Unit ): Boolean = value || { body ; true }
98- inline def but (inline body : => Unit ): Boolean = value && { body ; false }
99100 inline def and (inline body : => Unit ): Boolean = value && { body ; true }
101+ inline def but (inline body : => Unit ): Boolean = value && { body ; false }
100102
101- /** A conversion specifier matched in the argi'th string part,
102- * with `argc` arguments to interpolate.
103- */
104- sealed abstract class Conversion :
105- // the match for this descriptor
106- def descriptor : Match
107- // the part number for reporting errors
108- def argi : Int
103+ enum Kind :
104+ case StringXn , HashXn , BooleanXn , CharacterXn , IntegralXn , FloatingPointXn , DateTimeXn , LiteralXn , ErrorXn
105+ import Kind .*
109106
107+ /** A conversion specifier matched in the argi'th string part, with `argc` arguments to interpolate.
108+ */
109+ final class Conversion (val descriptor : Match , val argi : Int , val kind : Kind ):
110110 // the descriptor fields
111111 val index : Option [Int ] = descriptor.intOf(Index )
112112 val flags : String = descriptor.stringOf(Flags )
@@ -115,26 +115,86 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
115115 val op : String = descriptor.stringOf(CC )
116116
117117 // the conversion char is the head of the op string (but see DateTimeXn)
118- val cc : Char = if isError then '?' else op(0 )
118+ val cc : Char =
119+ kind match
120+ case ErrorXn => '?'
121+ case DateTimeXn => if op.length > 1 then op(1 ) else '?'
122+ case _ => op(0 )
119123
120- def isError : Boolean = false
121124 def isIndexed : Boolean = index.nonEmpty || hasFlag('<' )
122- def isLiteral : Boolean = false
125+ def isError : Boolean = kind == ErrorXn
126+ def isLiteral : Boolean = kind == LiteralXn
123127
124128 // descriptor is at index 0 of the part string
125129 def isLeading : Boolean = descriptor.at(Spec ) == 0
126130
131+ // flags and index in specifier are ok
132+ private def goodies = goodFlags && goodIndex
133+
127134 // true if passes. Default checks flags and index
128- def verify : Boolean = goodFlags && goodIndex
135+ def verify : Boolean =
136+ kind match {
137+ case StringXn => goodies
138+ case BooleanXn => goodies
139+ case HashXn => goodies
140+ case CharacterXn => goodies && noPrecision && only_-(" c conversion" )
141+ case IntegralXn =>
142+ def d_# = cc == 'd' && hasFlag('#' ) and badFlag('#' , " # not allowed for d conversion" )
143+ def x_comma = cc != 'd' && hasFlag(',' ) and badFlag(',' , " ',' only allowed for d conversion of integral types" )
144+ goodies && noPrecision && ! d_# && ! x_comma
145+ case FloatingPointXn =>
146+ goodies && (cc match {
147+ case 'a' | 'A' =>
148+ val badFlags = " ,(" .filter(hasFlag)
149+ noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s " ' $badf' not allowed for a, A " ))
150+ case _ => true
151+ })
152+ case DateTimeXn =>
153+ def hasCC = op.length == 2 or errorAt(CC )(" Date/time conversion must have two characters" )
154+ def goodCC = " HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc" .contains(cc) or errorAt(CC , 1 )(s " ' $cc' doesn't seem to be a date or time conversion " )
155+ goodies && hasCC && goodCC && noPrecision && only_-(" date/time conversions" )
156+ case LiteralXn =>
157+ op match {
158+ case " %" => goodies && noPrecision and width.foreach(_ => warningAt(Width )(" width ignored on literal" ))
159+ case " n" => noFlags && noWidth && noPrecision
160+ }
161+ case ErrorXn =>
162+ errorAt(CC )(s " illegal conversion character ' $cc' " )
163+ false
164+ }
129165
130166 // is the specifier OK with the given arg
131- def accepts (arg : ClassTag [? ]): Boolean = true
167+ def accepts (arg : ClassTag [? ]): Boolean =
168+ kind match
169+ case BooleanXn => arg == classTag[Boolean ] orElse warningAt(CC )(" Boolean format is null test for non-Boolean" )
170+ case IntegralXn =>
171+ arg == classTag[BigInt ] || ! cond(cc) {
172+ case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) => " + (" .filter(hasFlag).foreach(bad => badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )) ; true
173+ }
174+ case _ => true
132175
133176 // what arg type if any does the conversion accept
134- def acceptableVariants : List [ClassTag [? ]]
177+ def acceptableVariants : List [ClassTag [? ]] =
178+ kind match {
179+ case StringXn => if hasFlag('#' ) then classTag[Formattable ] :: Nil else classTag[Any ] :: Nil
180+ case BooleanXn => classTag[Boolean ] :: Conversion .FakeNullTag :: Nil
181+ case HashXn => classTag[Any ] :: Nil
182+ case CharacterXn => classTag[Char ] :: classTag[Byte ] :: classTag[Short ] :: classTag[Int ] :: Nil
183+ case IntegralXn => classTag[Int ] :: classTag[Long ] :: classTag[Byte ] :: classTag[Short ] :: classTag[BigInt ] :: Nil
184+ case FloatingPointXn => classTag[Double ] :: classTag[Float ] :: classTag[BigDecimal ] :: Nil
185+ case DateTimeXn => classTag[Long ] :: classTag[Calendar ] :: classTag[Date ] :: Nil
186+ case LiteralXn => Nil
187+ case ErrorXn => Nil
188+ }
135189
136- // what flags does the conversion accept? defaults to all
137- protected def okFlags : String = allFlags
190+ // what flags does the conversion accept?
191+ private def okFlags : String =
192+ kind match {
193+ case StringXn => " -#<"
194+ case BooleanXn | HashXn => " -<"
195+ case LiteralXn => " -"
196+ case _ => " -#+ 0,(<"
197+ }
138198
139199 def hasFlag (f : Char ) = flags.contains(f)
140200 def hasAnyFlag (fs : String ) = fs.exists(hasFlag)
@@ -146,6 +206,7 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
146206 def errorAt (g : SpecGroup , i : Int = 0 )(msg : String ) = reporter.partError(msg, argi, descriptor.offset(g, i))
147207 def warningAt (g : SpecGroup , i : Int = 0 )(msg : String ) = reporter.partWarning(msg, argi, descriptor.offset(g, i))
148208
209+ // various assertions
149210 def noFlags = flags.isEmpty or errorAt(Flags )(" flags not allowed" )
150211 def noWidth = width.isEmpty or errorAt(Width )(" width not allowed" )
151212 def noPrecision = precision.isEmpty or errorAt(Precision )(" precision not allowed" )
@@ -162,84 +223,25 @@ abstract class FormatChecker(using reporter: InterpolationReporter):
162223 okRange || hasFlag('<' ) or errorAt(Index )(" Argument index out of range" )
163224 object Conversion :
164225 def apply (m : Match , i : Int ): Conversion =
165- def badCC (msg : String ) = ErrorXn (m, i).tap(error => error.errorAt(if (error.op.isEmpty) Spec else CC )(msg))
166- def cv (cc : Char ) = cc match
167- case 's' | 'S' => StringXn (m, i)
168- case 'h' | 'H' => HashXn (m, i)
169- case 'b' | 'B' => BooleanXn (m, i)
170- case 'c' | 'C' => CharacterXn (m, i)
226+ def kindOf (cc : Char ) = cc match
227+ case 's' | 'S' => StringXn
228+ case 'h' | 'H' => HashXn
229+ case 'b' | 'B' => BooleanXn
230+ case 'c' | 'C' => CharacterXn
171231 case 'd' | 'o' |
172- 'x' | 'X' => IntegralXn (m, i)
232+ 'x' | 'X' => IntegralXn
173233 case 'e' | 'E' |
174234 'f' |
175235 'g' | 'G' |
176- 'a' | 'A' => FloatingPointXn (m, i)
177- case 't' | 'T' => DateTimeXn (m, i)
178- case '%' | 'n' => LiteralXn (m, i)
179- case _ => badCC( s " illegal conversion character ' $cc ' " )
180- end cv
236+ 'a' | 'A' => FloatingPointXn
237+ case 't' | 'T' => DateTimeXn
238+ case '%' | 'n' => LiteralXn
239+ case _ => ErrorXn
240+ end kindOf
181241 m.group(CC ) match
182- case Some (cc) => cv( cc(0 )).tap(_.verify)
183- case None => badCC( s " Missing conversion operator in ' ${m.matched}'; $literalHelp" )
242+ case Some (cc) => new Conversion (m, i, kindOf( cc(0 ) )).tap(_.verify)
243+ case None => new Conversion (m, i, ErrorXn ).tap(_.errorAt( Spec )( s " Missing conversion operator in ' ${m.matched}'; $literalHelp" ) )
184244 end apply
185245 val literalHelp = " use %% for literal %, %n for newline"
246+ private val FakeNullTag : ClassTag [? ] = null
186247 end Conversion
187- abstract class GeneralXn extends Conversion
188- // s | S
189- class StringXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
190- val acceptableVariants =
191- if hasFlag('#' ) then classTag[Formattable ] :: Nil
192- else classTag[Any ] :: Nil
193- override protected def okFlags = " -#<"
194- // b | B
195- class BooleanXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
196- val FakeNullTag : ClassTag [? ] = null
197- val acceptableVariants = classTag[Boolean ] :: FakeNullTag :: Nil
198- override def accepts (arg : ClassTag [? ]): Boolean =
199- arg == classTag[Boolean ] orElse warningAt(CC )(" Boolean format is null test for non-Boolean" )
200- override protected def okFlags = " -<"
201- // h | H
202- class HashXn (val descriptor : Match , val argi : Int ) extends GeneralXn :
203- val acceptableVariants = classTag[Any ] :: Nil
204- override protected def okFlags = " -<"
205- // %% | %n
206- class LiteralXn (val descriptor : Match , val argi : Int ) extends Conversion :
207- override def isLiteral = true
208- override def verify = op match
209- case " %" => super .verify && noPrecision and width.foreach(_ => warningAt(Width )(" width ignored on literal" ))
210- case " n" => noFlags && noWidth && noPrecision
211- override protected val okFlags = " -"
212- override def acceptableVariants = Nil
213- class CharacterXn (val descriptor : Match , val argi : Int ) extends Conversion :
214- override def verify = super .verify && noPrecision && only_-(" c conversion" )
215- val acceptableVariants = classTag[Char ] :: classTag[Byte ] :: classTag[Short ] :: classTag[Int ] :: Nil
216- class IntegralXn (val descriptor : Match , val argi : Int ) extends Conversion :
217- override def verify =
218- def d_# = cc == 'd' && hasFlag('#' ) and badFlag('#' , " # not allowed for d conversion" )
219- def x_comma = cc != 'd' && hasFlag(',' ) and badFlag(',' , " ',' only allowed for d conversion of integral types" )
220- super .verify && noPrecision && ! d_# && ! x_comma
221- val acceptableVariants = classTag[Int ] :: classTag[Long ] :: classTag[Byte ] :: classTag[Short ] :: classTag[BigInt ] :: Nil
222- override def accepts (arg : ClassTag [? ]): Boolean =
223- arg == classTag[BigInt ] || {
224- cc match
225- case 'o' | 'x' | 'X' if hasAnyFlag(" + (" ) => " + (" .filter(hasFlag).foreach(bad => badFlag(bad, s " only use ' $bad' for BigInt conversions to o, x, X " )) ; false
226- case _ => true
227- }
228- class FloatingPointXn (val descriptor : Match , val argi : Int ) extends Conversion :
229- override def verify = super .verify && (cc match {
230- case 'a' | 'A' =>
231- val badFlags = " ,(" .filter(hasFlag)
232- noPrecision && badFlags.isEmpty or badFlags.foreach(badf => badFlag(badf, s " ' $badf' not allowed for a, A " ))
233- case _ => true
234- })
235- val acceptableVariants = classTag[Double ] :: classTag[Float ] :: classTag[BigDecimal ] :: Nil
236- class DateTimeXn (val descriptor : Match , val argi : Int ) extends Conversion :
237- override val cc : Char = if op.length > 1 then op(1 ) else '?'
238- def hasCC = op.length == 2 or errorAt(CC )(" Date/time conversion must have two characters" )
239- def goodCC = " HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc" .contains(cc) or errorAt(CC , 1 )(s " ' $cc' doesn't seem to be a date or time conversion " )
240- override def verify = super .verify && hasCC && goodCC && noPrecision && only_-(" date/time conversions" )
241- val acceptableVariants = classTag[Long ] :: classTag[Calendar ] :: classTag[Date ] :: Nil
242- class ErrorXn (val descriptor : Match , val argi : Int ) extends Conversion :
243- override def isError = true
244- override def verify = false
245- override def acceptableVariants = Nil
0 commit comments