summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorVindaar <basti90@gmail.com>2020-04-22 10:41:56 +0200
committerGitHub <noreply@github.com>2020-04-22 09:41:56 +0100
commitd42c5a575dfaa6df363fd47c95d13c70aa60729c (patch)
treea8762ad78d20a9a5c1891babd33fb6b5ffb497f6
parentc3f4b93060b5e72b5ee1a4e37d5c69e63e7d8bb8 (diff)
downloadNim-d42c5a575dfaa6df363fd47c95d13c70aa60729c.tar.gz
base `parseEnum` on a case statement, fixes #14030 (#14046)
* base `parseEnum` on a case statement, fixes #14030

* apply simplifactions / clean up, remove `norm` node, use strVal

* export `normalize` in json.nim

* cmp using nimIdentNormalize, error at CT if ambiguous enum found

`nimIdentNormalize` provided by @cooldome.

We track all names of the branches we have created so far and error if
a duplicate is found.

Dummy change to make github react...

* fix docstring of `nimIdentNormalize`

* make `typ` arg `typedesc`, add lineinfo, call norm. only once
-rw-r--r--lib/pure/json.nim2
-rw-r--r--lib/pure/strutils.nim93
-rw-r--r--tests/stdlib/tstrutil.nim87
3 files changed, 171 insertions, 11 deletions
diff --git a/lib/pure/json.nim b/lib/pure/json.nim
index 7921c0c05..20608edef 100644
--- a/lib/pure/json.nim
+++ b/lib/pure/json.nim
@@ -158,7 +158,7 @@ export
 export
   parsejson.JsonEventKind, parsejson.JsonError, JsonParser, JsonKindError,
   open, close, str, getInt, getFloat, kind, getColumn, getLine, getFilename,
-  errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr
+  errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr, nimIdentNormalize
 
 type
   JsonNodeKind* = enum ## possible JSON node types
diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim
index 535af2b31..ff76fa94f 100644
--- a/lib/pure/strutils.nim
+++ b/lib/pure/strutils.nim
@@ -75,6 +75,7 @@
 import parseutils
 from math import pow, floor, log10
 from algorithm import reverse
+import macros # for `parseEnum`
 
 when defined(nimVmExportFixed):
   from unicode import toLower, toUpper
@@ -278,6 +279,26 @@ proc capitalizeAscii*(s: string): string {.noSideEffect, procvar,
   if s.len == 0: result = ""
   else: result = toUpperAscii(s[0]) & substr(s, 1)
 
+proc nimIdentNormalize*(s: string): string =
+  ## Normalizes the string `s` as a Nim identifier.
+  ##
+  ## That means to convert to lower case and remove any '_' on all characters
+  ## except first one.
+  runnableExamples:
+    doAssert nimIdentNormalize("Foo_bar") == "Foobar"
+  result = newString(s.len)
+  if s.len > 0:
+    result[0] = s[0]
+  var j = 1
+  for i in 1..len(s) - 1:
+    if s[i] in {'A'..'Z'}:
+      result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
+      inc j
+    elif s[i] != '_':
+      result[j] = s[i]
+      inc j
+  if j != s.len: setLen(result, j)
+
 proc normalize*(s: string): string {.noSideEffect, procvar,
   rtl, extern: "nsuNormalize".} =
   ## Normalizes the string `s`.
@@ -1229,8 +1250,65 @@ proc parseBool*(s: string): bool =
   of "n", "no", "false", "0", "off": result = false
   else: raise newException(ValueError, "cannot interpret as a bool: " & s)
 
+proc addOfBranch(s: string, field, enumType: NimNode): NimNode =
+  result = nnkOfBranch.newTree(
+    newLit s,
+    nnkCall.newTree(enumType, field) # `T(<fieldValue>)`
+  )
+
+macro genEnumStmt(typ: typedesc, argSym: typed, default: typed): untyped =
+  # generates a case stmt, which assigns the correct enum field given
+  # a normalized string comparison to the `argSym` input.
+  # NOTE: for an enum with fields Foo, Bar, ... we cannot generate
+  # `of "Foo".nimIdentNormalize: Foo`.
+  # This will fail, if the enum is not defined at top level (e.g. in a block).
+  # Thus we check for the field value of the (possible holed enum) and convert
+  # the integer value to the generic argument `typ`.
+  let typ = typ.getTypeInst[1]
+  let impl = typ.getImpl[2]
+  expectKind impl, nnkEnumTy
+  result = nnkCaseStmt.newTree(nnkDotExpr.newTree(argSym,
+                                                  bindSym"nimIdentNormalize"))
+  # stores all processed field strings to give error msg for ambiguous enums
+  var foundFields: seq[string]
+  var fStr: string # string of current field
+  var fNum: BiggestInt # int value of current field
+  for f in impl:
+    case f.kind
+    of nnkEmpty: continue # skip first node of `enumTy`
+    of nnkSym, nnkIdent: fStr = f.strVal
+    of nnkEnumFieldDef:
+      case f[1].kind
+      of nnkStrLit: fStr = f[1].strVal
+      of nnkTupleConstr:
+        fStr = f[1][1].strVal
+        fNum = f[1][0].intVal
+      of nnkIntLit:
+        fStr = f[0].strVal
+        fNum = f[1].intVal
+      else: error("Invalid tuple syntax!", f[1])
+    else: error("Invalid node for enum type!", f)
+    # add field if string not already added
+    fStr = nimIdentNormalize(fStr)
+    if fStr notin foundFields:
+      result.add addOfBranch(fStr, newLit fNum, typ)
+      foundFields.add fStr
+    else:
+      error("Ambiguous enums cannot be parsed, field " & $fStr &
+        " appears multiple times!")
+    inc fNum
+  # finally add else branch to raise or use default
+  if default == nil:
+    let raiseStmt = quote do:
+      raise newException(ValueError, "Invalid enum value: " & $`argSym`)
+    result.add nnkElse.newTree(raiseStmt)
+  else:
+    expectKind(default, nnkSym)
+    result.add nnkElse.newTree(default)
+
 proc parseEnum*[T: enum](s: string): T =
-  ## Parses an enum ``T``.
+  ## Parses an enum ``T``. This errors at compile time, if the given enum
+  ## type contains multiple fields with the same string value.
   ##
   ## Raises ``ValueError`` for an invalid value in `s`. The comparison is
   ## done in a style insensitive way.
@@ -1246,13 +1324,11 @@ proc parseEnum*[T: enum](s: string): T =
     doAssertRaises(ValueError):
       echo parseEnum[MyEnum]("third")
 
-  for e in low(T)..high(T):
-    if cmpIgnoreStyle(s, $e) == 0:
-      return e
-  raise newException(ValueError, "invalid enum value: " & s)
+  genEnumStmt(T, s, default = nil)
 
 proc parseEnum*[T: enum](s: string, default: T): T =
-  ## Parses an enum ``T``.
+  ## Parses an enum ``T``. This errors at compile time, if the given enum
+  ## type contains multiple fields with the same string value.
   ##
   ## Uses `default` for an invalid value in `s`. The comparison is done in a
   ## style insensitive way.
@@ -1267,10 +1343,7 @@ proc parseEnum*[T: enum](s: string, default: T): T =
     doAssert parseEnum[MyEnum]("second") == second
     doAssert parseEnum[MyEnum]("last", third) == third
 
-  for e in low(T)..high(T):
-    if cmpIgnoreStyle(s, $e) == 0:
-      return e
-  result = default
+  genEnumStmt(T, s, default)
 
 proc repeat*(c: char, count: Natural): string {.noSideEffect,
   rtl, extern: "nsuRepeatChar".} =
diff --git a/tests/stdlib/tstrutil.nim b/tests/stdlib/tstrutil.nim
index 9dbc98912..eb0fdbfa5 100644
--- a/tests/stdlib/tstrutil.nim
+++ b/tests/stdlib/tstrutil.nim
@@ -348,3 +348,90 @@ when true:
 
 main()
 #OUT ha/home/a1xyz/usr/bin
+
+
+# `parseEnum`, ref issue #14030
+# check enum defined at top level
+type
+  Foo = enum
+    A
+    B = "bb"
+    C = (5, "ccc")
+    D = 15
+    E = "ee" # check that we count enum fields correctly
+
+block:
+  let a = parseEnum[Foo]("A")
+  let b = parseEnum[Foo]("bb")
+  let c = parseEnum[Foo]("ccc")
+  let d = parseEnum[Foo]("D")
+  let e = parseEnum[Foo]("ee")
+  doAssert a == A
+  doAssert b == B
+  doAssert c == C
+  doAssert d == D
+  doAssert e == E
+  try:
+    let f = parseEnum[Foo]("Bar")
+    doAssert false
+  except ValueError:
+    discard
+
+  # finally using default
+  let g = parseEnum[Foo]("Bar", A)
+  doAssert g == A
+
+block:
+  # check enum defined in block
+  type
+    Bar = enum
+      V
+      W = "ww"
+      X = (3, "xx")
+      Y = 10
+      Z = "zz" # check that we count enum fields correctly
+
+  let a = parseEnum[Bar]("V")
+  let b = parseEnum[Bar]("ww")
+  let c = parseEnum[Bar]("xx")
+  let d = parseEnum[Bar]("Y")
+  let e = parseEnum[Bar]("zz")
+  doAssert a == V
+  doAssert b == W
+  doAssert c == X
+  doAssert d == Y
+  doAssert e == Z
+  try:
+    let f = parseEnum[Bar]("Baz")
+    doAssert false
+  except ValueError:
+    discard
+
+  # finally using default
+  let g = parseEnum[Bar]("Baz", V)
+  doAssert g == V
+
+block:
+  # check ambiguous enum fails to parse
+  type
+    Ambig = enum
+      f1 = "A"
+      f2 = "B"
+      f3 = "A"
+
+  doAssert not compiles((let a = parseEnum[Ambig]("A")))
+
+block:
+  # check almost ambiguous enum
+  type
+    AlmostAmbig = enum
+      f1 = "someA"
+      f2 = "someB"
+      f3 = "SomeA"
+
+  let a = parseEnum[AlmostAmbig]("someA")
+  let b = parseEnum[AlmostAmbig]("someB")
+  let c = parseEnum[AlmostAmbig]("SomeA")
+  doAssert a == f1
+  doAssert b == f2
+  doAssert c == f3