11from typing import Any
22
33from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
4- from .Literals import Language , LiteralFormat , LiteralValue
4+ from .Literals import Language , LiteralFormat , LiteralRepr , LiteralValue
55from .Slice import Slice
66
77
@@ -78,21 +78,25 @@ def _get_arg(self):
7878
7979 def set_literal_and_get_newline_delta (self , literal_value : LiteralValue ) -> int :
8080 encoded = literal_value .format .encode (
81- literal_value .actual , self .__language , self .__escape_leading_whitespace
81+ literal_value .actual ,
82+ self .__language ,
83+ self .__escape_leading_whitespace ,
8284 )
83- round_tripped = literal_value .format .parse (encoded , self .__language )
84- if round_tripped != literal_value .actual :
85- raise ValueError (
86- f"There is an error in { literal_value .format .__class__ .__name__ } , "
87- "the following value isn't round tripping.\n "
88- f"Please report this error and the data below at "
89- "https://github.com/diffplug/selfie/issues/new\n "
90- f"```\n "
91- f"ORIGINAL\n { literal_value .actual } \n "
92- f"ROUNDTRIPPED\n { round_tripped } \n "
93- f"ENCODED ORIGINAL\n { encoded } \n "
94- f"```\n "
95- )
85+ if not isinstance (literal_value .format , LiteralRepr ):
86+ # we don't roundtrip LiteralRepr because `eval` is dangerous
87+ round_tripped = literal_value .format .parse (encoded , self .__language )
88+ if round_tripped != literal_value .actual :
89+ raise ValueError (
90+ f"There is an error in { literal_value .format .__class__ .__name__ } , "
91+ "the following value isn't round tripping.\n "
92+ f"Please report this error and the data below at "
93+ "https://github.com/diffplug/selfie/issues/new\n "
94+ f"```\n "
95+ f"ORIGINAL\n { literal_value .actual } \n "
96+ f"ROUNDTRIPPED\n { round_tripped } \n "
97+ f"ENCODED ORIGINAL\n { encoded } \n "
98+ f"```\n "
99+ )
96100 existing_newlines = self .__function_call_plus_arg .count ("\n " )
97101 new_newlines = encoded .count ("\n " )
98102 self .__parent ._content_slice = Slice ( # noqa: SLF001
@@ -153,48 +157,98 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
153157 f"on line { line_one_indexed } "
154158 )
155159
156- end_arg = - 1
157- end_paren = 0
158160 if self ._content_slice [arg_start ] == '"' :
159- if self ._content_slice .subSequence (
160- arg_start , len (self ._content_slice )
161- ).starts_with (self .TRIPLE_QUOTE ):
162- end_arg = self ._content_slice .indexOf (
163- self .TRIPLE_QUOTE , arg_start + len (self .TRIPLE_QUOTE )
161+ (end_paren , end_arg ) = self ._parse_string (
162+ line_one_indexed , arg_start , dot_fun_open_paren
163+ )
164+ else :
165+ (end_paren , end_arg ) = self ._parse_code (
166+ line_one_indexed , arg_start , dot_fun_open_paren
167+ )
168+ return self .ToBeLiteral (
169+ self ,
170+ dot_fun_open_paren .replace ("_TODO" , "" ),
171+ self ._content_slice .subSequence (dot_function_call , end_paren + 1 ),
172+ self ._content_slice .subSequence (arg_start , end_arg ),
173+ self .__language ,
174+ self .__escape_leading_whitespace ,
175+ )
176+
177+ def _parse_code (
178+ self ,
179+ line_one_indexed : int ,
180+ arg_start : int ,
181+ dot_fun_open_paren : str ,
182+ ):
183+ # Initialize variables
184+ parenthesis_count = 1
185+ string_delimiter = None
186+
187+ # Iterate through the characters starting from the given index
188+ for i in range (arg_start , len (self ._content_slice )):
189+ char = self ._content_slice [i ]
190+
191+ # Check if we are entering or leaving a string
192+ if char in ["'" , '"' ] and self ._content_slice [i - 1 ] != "\\ " :
193+ if not string_delimiter :
194+ string_delimiter = char
195+ elif char == string_delimiter :
196+ string_delimiter = None
197+
198+ # Skip characters inside strings
199+ if string_delimiter :
200+ continue
201+
202+ # Count parentheses
203+ if char == "(" :
204+ parenthesis_count += 1
205+ elif char == ")" :
206+ parenthesis_count -= 1
207+
208+ # If all parentheses are closed, return the current index
209+ if parenthesis_count == 0 :
210+ end_paren = i
211+ end_arg = i - 1
212+ return (end_paren , end_arg )
213+ # else ...
214+ raise AssertionError (
215+ f"Appears to be an unclosed function call `{ dot_fun_open_paren } ` "
216+ f"starting at line { line_one_indexed } "
217+ )
218+
219+ def _parse_string (
220+ self ,
221+ line_one_indexed : int ,
222+ arg_start : int ,
223+ dot_fun_open_paren : str ,
224+ ):
225+ if self ._content_slice .subSequence (
226+ arg_start , len (self ._content_slice )
227+ ).starts_with (self .TRIPLE_QUOTE ):
228+ end_arg = self ._content_slice .indexOf (
229+ self .TRIPLE_QUOTE , arg_start + len (self .TRIPLE_QUOTE )
230+ )
231+ if end_arg == - 1 :
232+ raise AssertionError (
233+ f"Appears to be an unclosed multiline string literal `{ self .TRIPLE_QUOTE } ` "
234+ f"on line { line_one_indexed } "
164235 )
165- if end_arg == - 1 :
166- raise AssertionError (
167- f"Appears to be an unclosed multiline string literal `{ self .TRIPLE_QUOTE } ` "
168- f"on line { line_one_indexed } "
169- )
170- else :
171- end_arg += len (self .TRIPLE_QUOTE )
172- end_paren = end_arg
173236 else :
174- end_arg = arg_start + 1
175- while (
176- self ._content_slice [end_arg ] != '"'
177- or self ._content_slice [end_arg - 1 ] == "\\ "
178- ):
179- end_arg += 1
180- if end_arg == self ._content_slice .__len__ ():
181- raise AssertionError (
182- f'Appears to be an unclosed string literal `"` '
183- f"on line { line_one_indexed } "
184- )
185- end_arg += 1
237+ end_arg += len (self .TRIPLE_QUOTE )
186238 end_paren = end_arg
187239 else :
188- end_arg = arg_start
189- while not self ._content_slice [end_arg ].isspace ():
190- if self ._content_slice [end_arg ] == ")" :
191- break
240+ end_arg = arg_start + 1
241+ while (
242+ self ._content_slice [end_arg ] != '"'
243+ or self ._content_slice [end_arg - 1 ] == "\\ "
244+ ):
192245 end_arg += 1
193246 if end_arg == self ._content_slice .__len__ ():
194247 raise AssertionError (
195- f" Appears to be an unclosed numeric literal "
248+ f' Appears to be an unclosed string literal `"` '
196249 f"on line { line_one_indexed } "
197250 )
251+ end_arg += 1
198252 end_paren = end_arg
199253 while self ._content_slice [end_paren ] != ")" :
200254 if not self ._content_slice [end_paren ].isspace ():
@@ -210,14 +264,7 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral:
210264 f"Appears to be an unclosed function call `{ dot_fun_open_paren } ` "
211265 f"starting at line { line_one_indexed } "
212266 )
213- return self .ToBeLiteral (
214- self ,
215- dot_fun_open_paren .replace ("_TODO" , "" ),
216- self ._content_slice .subSequence (dot_function_call , end_paren + 1 ),
217- self ._content_slice .subSequence (arg_start , end_arg ),
218- self .__language ,
219- self .__escape_leading_whitespace ,
220- )
267+ return (end_paren , end_arg )
221268
222269
223270TO_BE_LIKES = [
0 commit comments