strformat: detect format string errors at compile-time (#23356)

This also prevents unwanted `raises: [ValueError]` effects from bubbling
up from correct format strings which makes `fmt` broadly unusable with
`raises`.

The old runtime-based `formatValue` overloads are kept for
backwards-compatibility, should anyone be using runtime format strings.

---------

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
(cherry picked from commit a1e41930f8)
This commit is contained in:
Jacek Sieka
2024-03-03 15:40:53 +01:00
committed by narimiran
parent 1ae6deda61
commit d389310bb9
2 changed files with 114 additions and 43 deletions

View File

@@ -472,6 +472,31 @@ proc parseStandardFormatSpecifier*(s: string; start = 0;
raise newException(ValueError,
"invalid format string, cannot parse: " & s[i..^1])
proc toRadix(typ: char): int =
case typ
of 'x', 'X': 16
of 'd', '\0': 10
of 'o': 8
of 'b': 2
else:
raise newException(ValueError,
"invalid type in format string for number, expected one " &
" of 'x', 'X', 'b', 'd', 'o' but got: " & typ)
proc formatValue*[T: SomeInteger](result: var string; value: T;
specifier: static string) =
## Standard format implementation for `SomeInteger`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
when specifier.len == 0:
result.add $value
else:
const
spec = parseStandardFormatSpecifier(specifier)
radix = toRadix(spec.typ)
result.add formatInt(value, radix, spec)
proc formatValue*[T: SomeInteger](result: var string; value: T;
specifier: string) =
## Standard format implementation for `SomeInteger`. It makes little
@@ -479,43 +504,16 @@ proc formatValue*[T: SomeInteger](result: var string; value: T;
## by the `&` macro.
if specifier.len == 0:
result.add $value
return
let spec = parseStandardFormatSpecifier(specifier)
var radix = 10
case spec.typ
of 'x', 'X': radix = 16
of 'd', '\0': discard
of 'b': radix = 2
of 'o': radix = 8
else:
raise newException(ValueError,
"invalid type in format string for number, expected one " &
" of 'x', 'X', 'b', 'd', 'o' but got: " & spec.typ)
result.add formatInt(value, radix, spec)
let
spec = parseStandardFormatSpecifier(specifier)
radix = toRadix(spec.typ)
proc formatValue*(result: var string; value: SomeFloat; specifier: string) =
## Standard format implementation for `SomeFloat`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
if specifier.len == 0:
result.add $value
return
let spec = parseStandardFormatSpecifier(specifier)
var fmode = ffDefault
case spec.typ
of 'e', 'E':
fmode = ffScientific
of 'f', 'F':
fmode = ffDecimal
of 'g', 'G':
fmode = ffDefault
of '\0': discard
else:
raise newException(ValueError,
"invalid type in format string for number, expected one " &
" of 'e', 'E', 'f', 'F', 'g', 'G' but got: " & spec.typ)
result.add formatInt(value, radix, spec)
proc formatFloat(
result: var string, value: SomeFloat, fmode: FloatFormatMode,
spec: StandardFormatSpecifier) =
var f = formatBiggestFloat(value, fmode, spec.precision)
var sign = false
if value >= 0.0:
@@ -550,23 +548,83 @@ proc formatValue*(result: var string; value: SomeFloat; specifier: string) =
else:
result.add res
proc toFloatFormatMode(typ: char): FloatFormatMode =
case typ
of 'e', 'E': ffScientific
of 'f', 'F': ffDecimal
of 'g', 'G': ffDefault
of '\0': ffDefault
else:
raise newException(ValueError,
"invalid type in format string for number, expected one " &
" of 'e', 'E', 'f', 'F', 'g', 'G' but got: " & typ)
proc formatValue*(result: var string; value: SomeFloat; specifier: static string) =
## Standard format implementation for `SomeFloat`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
when specifier.len == 0:
result.add $value
else:
const
spec = parseStandardFormatSpecifier(specifier)
fmode = toFloatFormatMode(spec.typ)
formatFloat(result, value, fmode, spec)
proc formatValue*(result: var string; value: SomeFloat; specifier: string) =
## Standard format implementation for `SomeFloat`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
if specifier.len == 0:
result.add $value
else:
let
spec = parseStandardFormatSpecifier(specifier)
fmode = toFloatFormatMode(spec.typ)
formatFloat(result, value, fmode, spec)
proc formatValue*(result: var string; value: string; specifier: static string) =
## Standard format implementation for `string`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
const spec = parseStandardFormatSpecifier(specifier)
var value =
when spec.typ in {'s', '\0'}: value
else: static:
raise newException(ValueError,
"invalid type in format string for string, expected 's', but got " &
spec.typ)
when spec.precision != -1:
if spec.precision < runeLen(value):
const precision = cast[Natural](spec.precision)
setLen(value, Natural(runeOffset(value, precision)))
result.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
proc formatValue*(result: var string; value: string; specifier: string) =
## Standard format implementation for `string`. It makes little
## sense to call this directly, but it is required to exist
## by the `&` macro.
let spec = parseStandardFormatSpecifier(specifier)
var value = value
case spec.typ
of 's', '\0': discard
else:
raise newException(ValueError,
"invalid type in format string for string, expected 's', but got " &
spec.typ)
var value =
if spec.typ in {'s', '\0'}: value
else:
raise newException(ValueError,
"invalid type in format string for string, expected 's', but got " &
spec.typ)
if spec.precision != -1:
if spec.precision < runeLen(value):
setLen(value, runeOffset(value, spec.precision))
let precision = cast[Natural](spec.precision)
setLen(value, Natural(runeOffset(value, precision)))
result.add alignString(value, spec.minimumWidth, spec.align, spec.fill)
proc formatValue[T: not SomeInteger](result: var string; value: T; specifier: static string) =
mixin `$`
formatValue(result, $value, specifier)
proc formatValue[T: not SomeInteger](result: var string; value: T; specifier: string) =
mixin `$`
formatValue(result, $value, specifier)

View File

@@ -562,7 +562,7 @@ proc main() =
doAssert &"""{(if true: "'" & "'" & ')' else: "")}""" == "'')"
doAssert &"{(if true: \"\'\" & \"'\" & ')' else: \"\")}" == "'')"
doAssert fmt"""{(if true: "'" & ')' else: "")}""" == "')"
block: # issue #20381
var ss: seq[string]
template myTemplate(s: string) =
@@ -573,5 +573,18 @@ proc main() =
foo()
doAssert ss == @["hello", "hello"]
block:
proc noraises() {.raises: [].} =
const
flt = 0.0
str = "str"
doAssert fmt"{flt} {str}" == "0.0 str"
noraises()
block:
doAssert not compiles(fmt"{formatting errors detected at compile time")
static: main()
main()