Skip to content

Commit

Permalink
Convert strings ourselves instead of using eval
Browse files Browse the repository at this point in the history
  • Loading branch information
Sunjay Cauligi committed Jul 29, 2023
1 parent 39be258 commit 522dba9
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 12 deletions.
4 changes: 4 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ New Features
------------------------------
* `defn`, `defn/a`, and `defclass` now support type parameters.

Bug Fixes
------------------------------
* Double quotes inside of bracketed f-strings are now properly handled.

0.27.0 (released 2023-07-06)
=============================

Expand Down
46 changes: 34 additions & 12 deletions hy/reader/hy_reader.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"Character reader for parsing Hy source."

import codecs
from itertools import islice

import hy
Expand Down Expand Up @@ -438,7 +439,7 @@ def delim_closing(c):
index = -1
return 0

return self.read_string_until(delim_closing, "fr" if is_fstring else None, is_fstring, brackets=delim)
return self.read_string_until(delim_closing, "r", is_fstring, brackets=delim)

def read_string_until(self, closing, prefix, is_fstring, **kwargs):
if is_fstring:
Expand All @@ -449,6 +450,7 @@ def read_string_until(self, closing, prefix, is_fstring, **kwargs):

def read_chars_until(self, closing, prefix, is_fstring):
s = []
in_named_escape = False
for c in self.chars():
s.append(c)
# check if c is closing
Expand All @@ -457,19 +459,39 @@ def read_chars_until(self, closing, prefix, is_fstring):
# string has ended
s = s[:-n_closing_chars]
break
# check if c is start of component
if is_fstring and c == "{" and s[-3:] != ["\\", "N", "{"]:
# check and handle "{{"
if self.peek_and_getc("{"):
s.append("{")
else:
# remove "{" from end of string component
s.pop()
break
if is_fstring:
# handle braces in f-strings
if c == "{":
if "r" not in prefix and s[-3:] == ["\\", "N", "{"]:
# ignore "\N{...}"
in_named_escape = True
else:
# start f-component if not "{{"
if not self.peek_and_getc("{"):
s.pop()
break
elif c == "}":
if in_named_escape:
in_named_escape = False
else:
if not self.peek_and_getc("}"):
raise SyntaxError("f-string: single '}' is not allowed")
res = "".join(s).replace("\x0d\x0a", "\x0a").replace("\x0d", "\x0a")

if prefix is not None:
res = eval(f'{prefix}"""{res}"""')
if "b" in prefix:
try:
res = res.encode('ascii')
except UnicodeEncodeError:
raise SyntaxError("bytes can only contain ASCII literal characters")

if "r" not in prefix:
# perform string escapes
if "b" in prefix:
res = codecs.escape_decode(res)[0]
else:
# https://stackoverflow.com/a/57192592
res = res.encode('latin1', errors='backslashreplace').decode('unicode_escape')

if is_fstring:
return res, n_closing_chars
return res
Expand Down
2 changes: 2 additions & 0 deletions tests/native_tests/strings.hy
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ cee"} dee" "ey bee\ncee dee"))
(assert (=
#[f[{{escaped braces}} \n {"not escaped"}]f]
"{escaped braces} \\n not escaped"))
; https://github.com/hylang/hy/issues/2474
(assert (= #[f["{0}"]f] "\"0\""))

; Quoting shouldn't evaluate the f-string immediately
; https://github.com/hylang/hy/issues/1844
Expand Down

0 comments on commit 522dba9

Please sign in to comment.