mirror of
https://github.com/nim-lang/Nim.git
synced 2026-02-17 08:34:20 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user