]> git.madduck.net Git - etc/vim.git/blob - src/black/trans.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Bump docker/build-push-action from 3 to 4 (#3549)
[etc/vim.git] / src / black / trans.py
1 """
2 String transformers that can split and merge strings.
3 """
4 import re
5 import sys
6 from abc import ABC, abstractmethod
7 from collections import defaultdict
8 from dataclasses import dataclass
9 from typing import (
10     Any,
11     Callable,
12     ClassVar,
13     Collection,
14     Dict,
15     Iterable,
16     Iterator,
17     List,
18     Optional,
19     Sequence,
20     Set,
21     Tuple,
22     TypeVar,
23     Union,
24 )
25
26 if sys.version_info < (3, 8):
27     from typing_extensions import Final, Literal
28 else:
29     from typing import Literal, Final
30
31 from mypy_extensions import trait
32
33 from black.comments import contains_pragma_comment
34 from black.lines import Line, append_leaves
35 from black.mode import Feature, Mode
36 from black.nodes import (
37     CLOSING_BRACKETS,
38     OPENING_BRACKETS,
39     STANDALONE_COMMENT,
40     is_empty_lpar,
41     is_empty_par,
42     is_empty_rpar,
43     is_part_of_annotation,
44     parent_type,
45     replace_child,
46     syms,
47 )
48 from black.rusty import Err, Ok, Result
49 from black.strings import (
50     assert_is_leaf_string,
51     get_string_prefix,
52     has_triple_quotes,
53     normalize_string_quotes,
54 )
55 from blib2to3.pgen2 import token
56 from blib2to3.pytree import Leaf, Node
57
58
59 class CannotTransform(Exception):
60     """Base class for errors raised by Transformers."""
61
62
63 # types
64 T = TypeVar("T")
65 LN = Union[Leaf, Node]
66 Transformer = Callable[[Line, Collection[Feature], Mode], Iterator[Line]]
67 Index = int
68 NodeType = int
69 ParserState = int
70 StringID = int
71 TResult = Result[T, CannotTransform]  # (T)ransform Result
72 TMatchResult = TResult[List[Index]]
73
74
75 def TErr(err_msg: str) -> Err[CannotTransform]:
76     """(T)ransform Err
77
78     Convenience function used when working with the TResult type.
79     """
80     cant_transform = CannotTransform(err_msg)
81     return Err(cant_transform)
82
83
84 def hug_power_op(
85     line: Line, features: Collection[Feature], mode: Mode
86 ) -> Iterator[Line]:
87     """A transformer which normalizes spacing around power operators."""
88
89     # Performance optimization to avoid unnecessary Leaf clones and other ops.
90     for leaf in line.leaves:
91         if leaf.type == token.DOUBLESTAR:
92             break
93     else:
94         raise CannotTransform("No doublestar token was found in the line.")
95
96     def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool:
97         # Brackets and parentheses indicate calls, subscripts, etc. ...
98         # basically stuff that doesn't count as "simple". Only a NAME lookup
99         # or dotted lookup (eg. NAME.NAME) is OK.
100         if step == -1:
101             disallowed = {token.RPAR, token.RSQB}
102         else:
103             disallowed = {token.LPAR, token.LSQB}
104
105         while 0 <= index < len(line.leaves):
106             current = line.leaves[index]
107             if current.type in disallowed:
108                 return False
109             if current.type not in {token.NAME, token.DOT} or current.value == "for":
110                 # If the current token isn't disallowed, we'll assume this is simple as
111                 # only the disallowed tokens are semantically attached to this lookup
112                 # expression we're checking. Also, stop early if we hit the 'for' bit
113                 # of a comprehension.
114                 return True
115
116             index += step
117
118         return True
119
120     def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
121         # An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple
122         # lookup (see above), with or without a preceding unary operator.
123         start = line.leaves[index]
124         if start.type in {token.NAME, token.NUMBER}:
125             return is_simple_lookup(index, step=(1 if kind == "exponent" else -1))
126
127         if start.type in {token.PLUS, token.MINUS, token.TILDE}:
128             if line.leaves[index + 1].type in {token.NAME, token.NUMBER}:
129                 # step is always one as bases with a preceding unary op will be checked
130                 # for simplicity starting from the next token (so it'll hit the check
131                 # above).
132                 return is_simple_lookup(index + 1, step=1)
133
134         return False
135
136     new_line = line.clone()
137     should_hug = False
138     for idx, leaf in enumerate(line.leaves):
139         new_leaf = leaf.clone()
140         if should_hug:
141             new_leaf.prefix = ""
142             should_hug = False
143
144         should_hug = (
145             (0 < idx < len(line.leaves) - 1)
146             and leaf.type == token.DOUBLESTAR
147             and is_simple_operand(idx - 1, kind="base")
148             and line.leaves[idx - 1].value != "lambda"
149             and is_simple_operand(idx + 1, kind="exponent")
150         )
151         if should_hug:
152             new_leaf.prefix = ""
153
154         # We have to be careful to make a new line properly:
155         # - bracket related metadata must be maintained (handled by Line.append)
156         # - comments need to copied over, updating the leaf IDs they're attached to
157         new_line.append(new_leaf, preformatted=True)
158         for comment_leaf in line.comments_after(leaf):
159             new_line.append(comment_leaf, preformatted=True)
160
161     yield new_line
162
163
164 class StringTransformer(ABC):
165     """
166     An implementation of the Transformer protocol that relies on its
167     subclasses overriding the template methods `do_match(...)` and
168     `do_transform(...)`.
169
170     This Transformer works exclusively on strings (for example, by merging
171     or splitting them).
172
173     The following sections can be found among the docstrings of each concrete
174     StringTransformer subclass.
175
176     Requirements:
177         Which requirements must be met of the given Line for this
178         StringTransformer to be applied?
179
180     Transformations:
181         If the given Line meets all of the above requirements, which string
182         transformations can you expect to be applied to it by this
183         StringTransformer?
184
185     Collaborations:
186         What contractual agreements does this StringTransformer have with other
187         StringTransfomers? Such collaborations should be eliminated/minimized
188         as much as possible.
189     """
190
191     __name__: Final = "StringTransformer"
192
193     # Ideally this would be a dataclass, but unfortunately mypyc breaks when used with
194     # `abc.ABC`.
195     def __init__(self, line_length: int, normalize_strings: bool) -> None:
196         self.line_length = line_length
197         self.normalize_strings = normalize_strings
198
199     @abstractmethod
200     def do_match(self, line: Line) -> TMatchResult:
201         """
202         Returns:
203             * Ok(string_indices) such that for each index, `line.leaves[index]`
204             is our target string if a match was able to be made. For
205             transformers that don't result in more lines (e.g. StringMerger,
206             StringParenStripper), multiple matches and transforms are done at
207             once to reduce the complexity.
208                 OR
209             * Err(CannotTransform), if no match could be made.
210         """
211
212     @abstractmethod
213     def do_transform(
214         self, line: Line, string_indices: List[int]
215     ) -> Iterator[TResult[Line]]:
216         """
217         Yields:
218             * Ok(new_line) where new_line is the new transformed line.
219                 OR
220             * Err(CannotTransform) if the transformation failed for some reason. The
221             `do_match(...)` template method should usually be used to reject
222             the form of the given Line, but in some cases it is difficult to
223             know whether or not a Line meets the StringTransformer's
224             requirements until the transformation is already midway.
225
226         Side Effects:
227             This method should NOT mutate @line directly, but it MAY mutate the
228             Line's underlying Node structure. (WARNING: If the underlying Node
229             structure IS altered, then this method should NOT be allowed to
230             yield an CannotTransform after that point.)
231         """
232
233     def __call__(
234         self, line: Line, _features: Collection[Feature], _mode: Mode
235     ) -> Iterator[Line]:
236         """
237         StringTransformer instances have a call signature that mirrors that of
238         the Transformer type.
239
240         Raises:
241             CannotTransform(...) if the concrete StringTransformer class is unable
242             to transform @line.
243         """
244         # Optimization to avoid calling `self.do_match(...)` when the line does
245         # not contain any string.
246         if not any(leaf.type == token.STRING for leaf in line.leaves):
247             raise CannotTransform("There are no strings in this line.")
248
249         match_result = self.do_match(line)
250
251         if isinstance(match_result, Err):
252             cant_transform = match_result.err()
253             raise CannotTransform(
254                 f"The string transformer {self.__class__.__name__} does not recognize"
255                 " this line as one that it can transform."
256             ) from cant_transform
257
258         string_indices = match_result.ok()
259
260         for line_result in self.do_transform(line, string_indices):
261             if isinstance(line_result, Err):
262                 cant_transform = line_result.err()
263                 raise CannotTransform(
264                     "StringTransformer failed while attempting to transform string."
265                 ) from cant_transform
266             line = line_result.ok()
267             yield line
268
269
270 @dataclass
271 class CustomSplit:
272     """A custom (i.e. manual) string split.
273
274     A single CustomSplit instance represents a single substring.
275
276     Examples:
277         Consider the following string:
278         ```
279         "Hi there friend."
280         " This is a custom"
281         f" string {split}."
282         ```
283
284         This string will correspond to the following three CustomSplit instances:
285         ```
286         CustomSplit(False, 16)
287         CustomSplit(False, 17)
288         CustomSplit(True, 16)
289         ```
290     """
291
292     has_prefix: bool
293     break_idx: int
294
295
296 @trait
297 class CustomSplitMapMixin:
298     """
299     This mixin class is used to map merged strings to a sequence of
300     CustomSplits, which will then be used to re-split the strings iff none of
301     the resultant substrings go over the configured max line length.
302     """
303
304     _Key: ClassVar = Tuple[StringID, str]
305     _CUSTOM_SPLIT_MAP: ClassVar[Dict[_Key, Tuple[CustomSplit, ...]]] = defaultdict(
306         tuple
307     )
308
309     @staticmethod
310     def _get_key(string: str) -> "CustomSplitMapMixin._Key":
311         """
312         Returns:
313             A unique identifier that is used internally to map @string to a
314             group of custom splits.
315         """
316         return (id(string), string)
317
318     def add_custom_splits(
319         self, string: str, custom_splits: Iterable[CustomSplit]
320     ) -> None:
321         """Custom Split Map Setter Method
322
323         Side Effects:
324             Adds a mapping from @string to the custom splits @custom_splits.
325         """
326         key = self._get_key(string)
327         self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
328
329     def pop_custom_splits(self, string: str) -> List[CustomSplit]:
330         """Custom Split Map Getter Method
331
332         Returns:
333             * A list of the custom splits that are mapped to @string, if any
334             exist.
335                 OR
336             * [], otherwise.
337
338         Side Effects:
339             Deletes the mapping between @string and its associated custom
340             splits (which are returned to the caller).
341         """
342         key = self._get_key(string)
343
344         custom_splits = self._CUSTOM_SPLIT_MAP[key]
345         del self._CUSTOM_SPLIT_MAP[key]
346
347         return list(custom_splits)
348
349     def has_custom_splits(self, string: str) -> bool:
350         """
351         Returns:
352             True iff @string is associated with a set of custom splits.
353         """
354         key = self._get_key(string)
355         return key in self._CUSTOM_SPLIT_MAP
356
357
358 class StringMerger(StringTransformer, CustomSplitMapMixin):
359     """StringTransformer that merges strings together.
360
361     Requirements:
362         (A) The line contains adjacent strings such that ALL of the validation checks
363         listed in StringMerger._validate_msg(...)'s docstring pass.
364             OR
365         (B) The line contains a string which uses line continuation backslashes.
366
367     Transformations:
368         Depending on which of the two requirements above where met, either:
369
370         (A) The string group associated with the target string is merged.
371             OR
372         (B) All line-continuation backslashes are removed from the target string.
373
374     Collaborations:
375         StringMerger provides custom split information to StringSplitter.
376     """
377
378     def do_match(self, line: Line) -> TMatchResult:
379         LL = line.leaves
380
381         is_valid_index = is_valid_index_factory(LL)
382
383         string_indices = []
384         idx = 0
385         while is_valid_index(idx):
386             leaf = LL[idx]
387             if (
388                 leaf.type == token.STRING
389                 and is_valid_index(idx + 1)
390                 and LL[idx + 1].type == token.STRING
391             ):
392                 if not is_part_of_annotation(leaf):
393                     string_indices.append(idx)
394
395                 # Advance to the next non-STRING leaf.
396                 idx += 2
397                 while is_valid_index(idx) and LL[idx].type == token.STRING:
398                     idx += 1
399
400             elif leaf.type == token.STRING and "\\\n" in leaf.value:
401                 string_indices.append(idx)
402                 # Advance to the next non-STRING leaf.
403                 idx += 1
404                 while is_valid_index(idx) and LL[idx].type == token.STRING:
405                     idx += 1
406
407             else:
408                 idx += 1
409
410         if string_indices:
411             return Ok(string_indices)
412         else:
413             return TErr("This line has no strings that need merging.")
414
415     def do_transform(
416         self, line: Line, string_indices: List[int]
417     ) -> Iterator[TResult[Line]]:
418         new_line = line
419
420         rblc_result = self._remove_backslash_line_continuation_chars(
421             new_line, string_indices
422         )
423         if isinstance(rblc_result, Ok):
424             new_line = rblc_result.ok()
425
426         msg_result = self._merge_string_group(new_line, string_indices)
427         if isinstance(msg_result, Ok):
428             new_line = msg_result.ok()
429
430         if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
431             msg_cant_transform = msg_result.err()
432             rblc_cant_transform = rblc_result.err()
433             cant_transform = CannotTransform(
434                 "StringMerger failed to merge any strings in this line."
435             )
436
437             # Chain the errors together using `__cause__`.
438             msg_cant_transform.__cause__ = rblc_cant_transform
439             cant_transform.__cause__ = msg_cant_transform
440
441             yield Err(cant_transform)
442         else:
443             yield Ok(new_line)
444
445     @staticmethod
446     def _remove_backslash_line_continuation_chars(
447         line: Line, string_indices: List[int]
448     ) -> TResult[Line]:
449         """
450         Merge strings that were split across multiple lines using
451         line-continuation backslashes.
452
453         Returns:
454             Ok(new_line), if @line contains backslash line-continuation
455             characters.
456                 OR
457             Err(CannotTransform), otherwise.
458         """
459         LL = line.leaves
460
461         indices_to_transform = []
462         for string_idx in string_indices:
463             string_leaf = LL[string_idx]
464             if (
465                 string_leaf.type == token.STRING
466                 and "\\\n" in string_leaf.value
467                 and not has_triple_quotes(string_leaf.value)
468             ):
469                 indices_to_transform.append(string_idx)
470
471         if not indices_to_transform:
472             return TErr(
473                 "Found no string leaves that contain backslash line continuation"
474                 " characters."
475             )
476
477         new_line = line.clone()
478         new_line.comments = line.comments.copy()
479         append_leaves(new_line, line, LL)
480
481         for string_idx in indices_to_transform:
482             new_string_leaf = new_line.leaves[string_idx]
483             new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
484
485         return Ok(new_line)
486
487     def _merge_string_group(
488         self, line: Line, string_indices: List[int]
489     ) -> TResult[Line]:
490         """
491         Merges string groups (i.e. set of adjacent strings).
492
493         Each index from `string_indices` designates one string group's first
494         leaf in `line.leaves`.
495
496         Returns:
497             Ok(new_line), if ALL of the validation checks found in
498             _validate_msg(...) pass.
499                 OR
500             Err(CannotTransform), otherwise.
501         """
502         LL = line.leaves
503
504         is_valid_index = is_valid_index_factory(LL)
505
506         # A dict of {string_idx: tuple[num_of_strings, string_leaf]}.
507         merged_string_idx_dict: Dict[int, Tuple[int, Leaf]] = {}
508         for string_idx in string_indices:
509             vresult = self._validate_msg(line, string_idx)
510             if isinstance(vresult, Err):
511                 continue
512             merged_string_idx_dict[string_idx] = self._merge_one_string_group(
513                 LL, string_idx, is_valid_index
514             )
515
516         if not merged_string_idx_dict:
517             return TErr("No string group is merged")
518
519         # Build the final line ('new_line') that this method will later return.
520         new_line = line.clone()
521         previous_merged_string_idx = -1
522         previous_merged_num_of_strings = -1
523         for i, leaf in enumerate(LL):
524             if i in merged_string_idx_dict:
525                 previous_merged_string_idx = i
526                 previous_merged_num_of_strings, string_leaf = merged_string_idx_dict[i]
527                 new_line.append(string_leaf)
528
529             if (
530                 previous_merged_string_idx
531                 <= i
532                 < previous_merged_string_idx + previous_merged_num_of_strings
533             ):
534                 for comment_leaf in line.comments_after(LL[i]):
535                     new_line.append(comment_leaf, preformatted=True)
536                 continue
537
538             append_leaves(new_line, line, [leaf])
539
540         return Ok(new_line)
541
542     def _merge_one_string_group(
543         self, LL: List[Leaf], string_idx: int, is_valid_index: Callable[[int], bool]
544     ) -> Tuple[int, Leaf]:
545         """
546         Merges one string group where the first string in the group is
547         `LL[string_idx]`.
548
549         Returns:
550             A tuple of `(num_of_strings, leaf)` where `num_of_strings` is the
551             number of strings merged and `leaf` is the newly merged string
552             to be replaced in the new line.
553         """
554         # If the string group is wrapped inside an Atom node, we must make sure
555         # to later replace that Atom with our new (merged) string leaf.
556         atom_node = LL[string_idx].parent
557
558         # We will place BREAK_MARK in between every two substrings that we
559         # merge. We will then later go through our final result and use the
560         # various instances of BREAK_MARK we find to add the right values to
561         # the custom split map.
562         BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
563
564         QUOTE = LL[string_idx].value[-1]
565
566         def make_naked(string: str, string_prefix: str) -> str:
567             """Strip @string (i.e. make it a "naked" string)
568
569             Pre-conditions:
570                 * assert_is_leaf_string(@string)
571
572             Returns:
573                 A string that is identical to @string except that
574                 @string_prefix has been stripped, the surrounding QUOTE
575                 characters have been removed, and any remaining QUOTE
576                 characters have been escaped.
577             """
578             assert_is_leaf_string(string)
579             if "f" in string_prefix:
580                 string = _toggle_fexpr_quotes(string, QUOTE)
581                 # After quotes toggling, quotes in expressions won't be escaped
582                 # because quotes can't be reused in f-strings. So we can simply
583                 # let the escaping logic below run without knowing f-string
584                 # expressions.
585
586             RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
587             naked_string = string[len(string_prefix) + 1 : -1]
588             naked_string = re.sub(
589                 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
590             )
591             return naked_string
592
593         # Holds the CustomSplit objects that will later be added to the custom
594         # split map.
595         custom_splits = []
596
597         # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
598         prefix_tracker = []
599
600         # Sets the 'prefix' variable. This is the prefix that the final merged
601         # string will have.
602         next_str_idx = string_idx
603         prefix = ""
604         while (
605             not prefix
606             and is_valid_index(next_str_idx)
607             and LL[next_str_idx].type == token.STRING
608         ):
609             prefix = get_string_prefix(LL[next_str_idx].value).lower()
610             next_str_idx += 1
611
612         # The next loop merges the string group. The final string will be
613         # contained in 'S'.
614         #
615         # The following convenience variables are used:
616         #
617         #   S: string
618         #   NS: naked string
619         #   SS: next string
620         #   NSS: naked next string
621         S = ""
622         NS = ""
623         num_of_strings = 0
624         next_str_idx = string_idx
625         while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
626             num_of_strings += 1
627
628             SS = LL[next_str_idx].value
629             next_prefix = get_string_prefix(SS).lower()
630
631             # If this is an f-string group but this substring is not prefixed
632             # with 'f'...
633             if "f" in prefix and "f" not in next_prefix:
634                 # Then we must escape any braces contained in this substring.
635                 SS = re.sub(r"(\{|\})", r"\1\1", SS)
636
637             NSS = make_naked(SS, next_prefix)
638
639             has_prefix = bool(next_prefix)
640             prefix_tracker.append(has_prefix)
641
642             S = prefix + QUOTE + NS + NSS + BREAK_MARK + QUOTE
643             NS = make_naked(S, prefix)
644
645             next_str_idx += 1
646
647         # Take a note on the index of the non-STRING leaf.
648         non_string_idx = next_str_idx
649
650         S_leaf = Leaf(token.STRING, S)
651         if self.normalize_strings:
652             S_leaf.value = normalize_string_quotes(S_leaf.value)
653
654         # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
655         temp_string = S_leaf.value[len(prefix) + 1 : -1]
656         for has_prefix in prefix_tracker:
657             mark_idx = temp_string.find(BREAK_MARK)
658             assert (
659                 mark_idx >= 0
660             ), "Logic error while filling the custom string breakpoint cache."
661
662             temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
663             breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
664             custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
665
666         string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
667
668         if atom_node is not None:
669             # If not all children of the atom node are merged (this can happen
670             # when there is a standalone comment in the middle) ...
671             if non_string_idx - string_idx < len(atom_node.children):
672                 # We need to replace the old STRING leaves with the new string leaf.
673                 first_child_idx = LL[string_idx].remove()
674                 for idx in range(string_idx + 1, non_string_idx):
675                     LL[idx].remove()
676                 if first_child_idx is not None:
677                     atom_node.insert_child(first_child_idx, string_leaf)
678             else:
679                 # Else replace the atom node with the new string leaf.
680                 replace_child(atom_node, string_leaf)
681
682         self.add_custom_splits(string_leaf.value, custom_splits)
683         return num_of_strings, string_leaf
684
685     @staticmethod
686     def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
687         """Validate (M)erge (S)tring (G)roup
688
689         Transform-time string validation logic for _merge_string_group(...).
690
691         Returns:
692             * Ok(None), if ALL validation checks (listed below) pass.
693                 OR
694             * Err(CannotTransform), if any of the following are true:
695                 - The target string group does not contain ANY stand-alone comments.
696                 - The target string is not in a string group (i.e. it has no
697                   adjacent strings).
698                 - The string group has more than one inline comment.
699                 - The string group has an inline comment that appears to be a pragma.
700                 - The set of all string prefixes in the string group is of
701                   length greater than one and is not equal to {"", "f"}.
702                 - The string group consists of raw strings.
703                 - The string group is stringified type annotations. We don't want to
704                   process stringified type annotations since pyright doesn't support
705                   them spanning multiple string values. (NOTE: mypy, pytype, pyre do
706                   support them, so we can change if pyright also gains support in the
707                   future. See https://github.com/microsoft/pyright/issues/4359.)
708         """
709         # We first check for "inner" stand-alone comments (i.e. stand-alone
710         # comments that have a string leaf before them AND after them).
711         for inc in [1, -1]:
712             i = string_idx
713             found_sa_comment = False
714             is_valid_index = is_valid_index_factory(line.leaves)
715             while is_valid_index(i) and line.leaves[i].type in [
716                 token.STRING,
717                 STANDALONE_COMMENT,
718             ]:
719                 if line.leaves[i].type == STANDALONE_COMMENT:
720                     found_sa_comment = True
721                 elif found_sa_comment:
722                     return TErr(
723                         "StringMerger does NOT merge string groups which contain "
724                         "stand-alone comments."
725                     )
726
727                 i += inc
728
729         num_of_inline_string_comments = 0
730         set_of_prefixes = set()
731         num_of_strings = 0
732         for leaf in line.leaves[string_idx:]:
733             if leaf.type != token.STRING:
734                 # If the string group is trailed by a comma, we count the
735                 # comments trailing the comma to be one of the string group's
736                 # comments.
737                 if leaf.type == token.COMMA and id(leaf) in line.comments:
738                     num_of_inline_string_comments += 1
739                 break
740
741             if has_triple_quotes(leaf.value):
742                 return TErr("StringMerger does NOT merge multiline strings.")
743
744             num_of_strings += 1
745             prefix = get_string_prefix(leaf.value).lower()
746             if "r" in prefix:
747                 return TErr("StringMerger does NOT merge raw strings.")
748
749             set_of_prefixes.add(prefix)
750
751             if id(leaf) in line.comments:
752                 num_of_inline_string_comments += 1
753                 if contains_pragma_comment(line.comments[id(leaf)]):
754                     return TErr("Cannot merge strings which have pragma comments.")
755
756         if num_of_strings < 2:
757             return TErr(
758                 f"Not enough strings to merge (num_of_strings={num_of_strings})."
759             )
760
761         if num_of_inline_string_comments > 1:
762             return TErr(
763                 f"Too many inline string comments ({num_of_inline_string_comments})."
764             )
765
766         if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
767             return TErr(f"Too many different prefixes ({set_of_prefixes}).")
768
769         return Ok(None)
770
771
772 class StringParenStripper(StringTransformer):
773     """StringTransformer that strips surrounding parentheses from strings.
774
775     Requirements:
776         The line contains a string which is surrounded by parentheses and:
777             - The target string is NOT the only argument to a function call.
778             - The target string is NOT a "pointless" string.
779             - If the target string contains a PERCENT, the brackets are not
780               preceded or followed by an operator with higher precedence than
781               PERCENT.
782
783     Transformations:
784         The parentheses mentioned in the 'Requirements' section are stripped.
785
786     Collaborations:
787         StringParenStripper has its own inherent usefulness, but it is also
788         relied on to clean up the parentheses created by StringParenWrapper (in
789         the event that they are no longer needed).
790     """
791
792     def do_match(self, line: Line) -> TMatchResult:
793         LL = line.leaves
794
795         is_valid_index = is_valid_index_factory(LL)
796
797         string_indices = []
798
799         idx = -1
800         while True:
801             idx += 1
802             if idx >= len(LL):
803                 break
804             leaf = LL[idx]
805
806             # Should be a string...
807             if leaf.type != token.STRING:
808                 continue
809
810             # If this is a "pointless" string...
811             if (
812                 leaf.parent
813                 and leaf.parent.parent
814                 and leaf.parent.parent.type == syms.simple_stmt
815             ):
816                 continue
817
818             # Should be preceded by a non-empty LPAR...
819             if (
820                 not is_valid_index(idx - 1)
821                 or LL[idx - 1].type != token.LPAR
822                 or is_empty_lpar(LL[idx - 1])
823             ):
824                 continue
825
826             # That LPAR should NOT be preceded by a function name or a closing
827             # bracket (which could be a function which returns a function or a
828             # list/dictionary that contains a function)...
829             if is_valid_index(idx - 2) and (
830                 LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS
831             ):
832                 continue
833
834             string_idx = idx
835
836             # Skip the string trailer, if one exists.
837             string_parser = StringParser()
838             next_idx = string_parser.parse(LL, string_idx)
839
840             # if the leaves in the parsed string include a PERCENT, we need to
841             # make sure the initial LPAR is NOT preceded by an operator with
842             # higher or equal precedence to PERCENT
843             if is_valid_index(idx - 2):
844                 # mypy can't quite follow unless we name this
845                 before_lpar = LL[idx - 2]
846                 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and (
847                     (
848                         before_lpar.type
849                         in {
850                             token.STAR,
851                             token.AT,
852                             token.SLASH,
853                             token.DOUBLESLASH,
854                             token.PERCENT,
855                             token.TILDE,
856                             token.DOUBLESTAR,
857                             token.AWAIT,
858                             token.LSQB,
859                             token.LPAR,
860                         }
861                     )
862                     or (
863                         # only unary PLUS/MINUS
864                         before_lpar.parent
865                         and before_lpar.parent.type == syms.factor
866                         and (before_lpar.type in {token.PLUS, token.MINUS})
867                     )
868                 ):
869                     continue
870
871             # Should be followed by a non-empty RPAR...
872             if (
873                 is_valid_index(next_idx)
874                 and LL[next_idx].type == token.RPAR
875                 and not is_empty_rpar(LL[next_idx])
876             ):
877                 # That RPAR should NOT be followed by anything with higher
878                 # precedence than PERCENT
879                 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in {
880                     token.DOUBLESTAR,
881                     token.LSQB,
882                     token.LPAR,
883                     token.DOT,
884                 }:
885                     continue
886
887                 string_indices.append(string_idx)
888                 idx = string_idx
889                 while idx < len(LL) - 1 and LL[idx + 1].type == token.STRING:
890                     idx += 1
891
892         if string_indices:
893             return Ok(string_indices)
894         return TErr("This line has no strings wrapped in parens.")
895
896     def do_transform(
897         self, line: Line, string_indices: List[int]
898     ) -> Iterator[TResult[Line]]:
899         LL = line.leaves
900
901         string_and_rpar_indices: List[int] = []
902         for string_idx in string_indices:
903             string_parser = StringParser()
904             rpar_idx = string_parser.parse(LL, string_idx)
905
906             should_transform = True
907             for leaf in (LL[string_idx - 1], LL[rpar_idx]):
908                 if line.comments_after(leaf):
909                     # Should not strip parentheses which have comments attached
910                     # to them.
911                     should_transform = False
912                     break
913             if should_transform:
914                 string_and_rpar_indices.extend((string_idx, rpar_idx))
915
916         if string_and_rpar_indices:
917             yield Ok(self._transform_to_new_line(line, string_and_rpar_indices))
918         else:
919             yield Err(
920                 CannotTransform("All string groups have comments attached to them.")
921             )
922
923     def _transform_to_new_line(
924         self, line: Line, string_and_rpar_indices: List[int]
925     ) -> Line:
926         LL = line.leaves
927
928         new_line = line.clone()
929         new_line.comments = line.comments.copy()
930
931         previous_idx = -1
932         # We need to sort the indices, since string_idx and its matching
933         # rpar_idx may not come in order, e.g. in
934         # `("outer" % ("inner".join(items)))`, the "inner" string's
935         # string_idx is smaller than "outer" string's rpar_idx.
936         for idx in sorted(string_and_rpar_indices):
937             leaf = LL[idx]
938             lpar_or_rpar_idx = idx - 1 if leaf.type == token.STRING else idx
939             append_leaves(new_line, line, LL[previous_idx + 1 : lpar_or_rpar_idx])
940             if leaf.type == token.STRING:
941                 string_leaf = Leaf(token.STRING, LL[idx].value)
942                 LL[lpar_or_rpar_idx].remove()  # Remove lpar.
943                 replace_child(LL[idx], string_leaf)
944                 new_line.append(string_leaf)
945             else:
946                 LL[lpar_or_rpar_idx].remove()  # This is a rpar.
947
948             previous_idx = idx
949
950         # Append the leaves after the last idx:
951         append_leaves(new_line, line, LL[idx + 1 :])
952
953         return new_line
954
955
956 class BaseStringSplitter(StringTransformer):
957     """
958     Abstract class for StringTransformers which transform a Line's strings by splitting
959     them or placing them on their own lines where necessary to avoid going over
960     the configured line length.
961
962     Requirements:
963         * The target string value is responsible for the line going over the
964         line length limit. It follows that after all of black's other line
965         split methods have been exhausted, this line (or one of the resulting
966         lines after all line splits are performed) would still be over the
967         line_length limit unless we split this string.
968             AND
969         * The target string is NOT a "pointless" string (i.e. a string that has
970         no parent or siblings).
971             AND
972         * The target string is not followed by an inline comment that appears
973         to be a pragma.
974             AND
975         * The target string is not a multiline (i.e. triple-quote) string.
976     """
977
978     STRING_OPERATORS: Final = [
979         token.EQEQUAL,
980         token.GREATER,
981         token.GREATEREQUAL,
982         token.LESS,
983         token.LESSEQUAL,
984         token.NOTEQUAL,
985         token.PERCENT,
986         token.PLUS,
987         token.STAR,
988     ]
989
990     @abstractmethod
991     def do_splitter_match(self, line: Line) -> TMatchResult:
992         """
993         BaseStringSplitter asks its clients to override this method instead of
994         `StringTransformer.do_match(...)`.
995
996         Follows the same protocol as `StringTransformer.do_match(...)`.
997
998         Refer to `help(StringTransformer.do_match)` for more information.
999         """
1000
1001     def do_match(self, line: Line) -> TMatchResult:
1002         match_result = self.do_splitter_match(line)
1003         if isinstance(match_result, Err):
1004             return match_result
1005
1006         string_indices = match_result.ok()
1007         assert len(string_indices) == 1, (
1008             f"{self.__class__.__name__} should only find one match at a time, found"
1009             f" {len(string_indices)}"
1010         )
1011         string_idx = string_indices[0]
1012         vresult = self._validate(line, string_idx)
1013         if isinstance(vresult, Err):
1014             return vresult
1015
1016         return match_result
1017
1018     def _validate(self, line: Line, string_idx: int) -> TResult[None]:
1019         """
1020         Checks that @line meets all of the requirements listed in this classes'
1021         docstring. Refer to `help(BaseStringSplitter)` for a detailed
1022         description of those requirements.
1023
1024         Returns:
1025             * Ok(None), if ALL of the requirements are met.
1026                 OR
1027             * Err(CannotTransform), if ANY of the requirements are NOT met.
1028         """
1029         LL = line.leaves
1030
1031         string_leaf = LL[string_idx]
1032
1033         max_string_length = self._get_max_string_length(line, string_idx)
1034         if len(string_leaf.value) <= max_string_length:
1035             return TErr(
1036                 "The string itself is not what is causing this line to be too long."
1037             )
1038
1039         if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
1040             token.STRING,
1041             token.NEWLINE,
1042         ]:
1043             return TErr(
1044                 f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
1045                 " no parent)."
1046             )
1047
1048         if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
1049             line.comments[id(line.leaves[string_idx])]
1050         ):
1051             return TErr(
1052                 "Line appears to end with an inline pragma comment. Splitting the line"
1053                 " could modify the pragma's behavior."
1054             )
1055
1056         if has_triple_quotes(string_leaf.value):
1057             return TErr("We cannot split multiline strings.")
1058
1059         return Ok(None)
1060
1061     def _get_max_string_length(self, line: Line, string_idx: int) -> int:
1062         """
1063         Calculates the max string length used when attempting to determine
1064         whether or not the target string is responsible for causing the line to
1065         go over the line length limit.
1066
1067         WARNING: This method is tightly coupled to both StringSplitter and
1068         (especially) StringParenWrapper. There is probably a better way to
1069         accomplish what is being done here.
1070
1071         Returns:
1072             max_string_length: such that `line.leaves[string_idx].value >
1073             max_string_length` implies that the target string IS responsible
1074             for causing this line to exceed the line length limit.
1075         """
1076         LL = line.leaves
1077
1078         is_valid_index = is_valid_index_factory(LL)
1079
1080         # We use the shorthand "WMA4" in comments to abbreviate "We must
1081         # account for". When giving examples, we use STRING to mean some/any
1082         # valid string.
1083         #
1084         # Finally, we use the following convenience variables:
1085         #
1086         #   P:  The leaf that is before the target string leaf.
1087         #   N:  The leaf that is after the target string leaf.
1088         #   NN: The leaf that is after N.
1089
1090         # WMA4 the whitespace at the beginning of the line.
1091         offset = line.depth * 4
1092
1093         if is_valid_index(string_idx - 1):
1094             p_idx = string_idx - 1
1095             if (
1096                 LL[string_idx - 1].type == token.LPAR
1097                 and LL[string_idx - 1].value == ""
1098                 and string_idx >= 2
1099             ):
1100                 # If the previous leaf is an empty LPAR placeholder, we should skip it.
1101                 p_idx -= 1
1102
1103             P = LL[p_idx]
1104             if P.type in self.STRING_OPERATORS:
1105                 # WMA4 a space and a string operator (e.g. `+ STRING` or `== STRING`).
1106                 offset += len(str(P)) + 1
1107
1108             if P.type == token.COMMA:
1109                 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
1110                 offset += 3
1111
1112             if P.type in [token.COLON, token.EQUAL, token.PLUSEQUAL, token.NAME]:
1113                 # This conditional branch is meant to handle dictionary keys,
1114                 # variable assignments, 'return STRING' statement lines, and
1115                 # 'else STRING' ternary expression lines.
1116
1117                 # WMA4 a single space.
1118                 offset += 1
1119
1120                 # WMA4 the lengths of any leaves that came before that space,
1121                 # but after any closing bracket before that space.
1122                 for leaf in reversed(LL[: p_idx + 1]):
1123                     offset += len(str(leaf))
1124                     if leaf.type in CLOSING_BRACKETS:
1125                         break
1126
1127         if is_valid_index(string_idx + 1):
1128             N = LL[string_idx + 1]
1129             if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
1130                 # If the next leaf is an empty RPAR placeholder, we should skip it.
1131                 N = LL[string_idx + 2]
1132
1133             if N.type == token.COMMA:
1134                 # WMA4 a single comma at the end of the string (e.g `STRING,`).
1135                 offset += 1
1136
1137             if is_valid_index(string_idx + 2):
1138                 NN = LL[string_idx + 2]
1139
1140                 if N.type == token.DOT and NN.type == token.NAME:
1141                     # This conditional branch is meant to handle method calls invoked
1142                     # off of a string literal up to and including the LPAR character.
1143
1144                     # WMA4 the '.' character.
1145                     offset += 1
1146
1147                     if (
1148                         is_valid_index(string_idx + 3)
1149                         and LL[string_idx + 3].type == token.LPAR
1150                     ):
1151                         # WMA4 the left parenthesis character.
1152                         offset += 1
1153
1154                     # WMA4 the length of the method's name.
1155                     offset += len(NN.value)
1156
1157         has_comments = False
1158         for comment_leaf in line.comments_after(LL[string_idx]):
1159             if not has_comments:
1160                 has_comments = True
1161                 # WMA4 two spaces before the '#' character.
1162                 offset += 2
1163
1164             # WMA4 the length of the inline comment.
1165             offset += len(comment_leaf.value)
1166
1167         max_string_length = self.line_length - offset
1168         return max_string_length
1169
1170     @staticmethod
1171     def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]:
1172         """
1173         Returns:
1174             string_idx such that @LL[string_idx] is equal to our target (i.e.
1175             matched) string, if this line matches the "prefer paren wrap" statement
1176             requirements listed in the 'Requirements' section of the StringParenWrapper
1177             class's docstring.
1178                 OR
1179             None, otherwise.
1180         """
1181         # The line must start with a string.
1182         if LL[0].type != token.STRING:
1183             return None
1184
1185         # If the string is surrounded by commas (or is the first/last child)...
1186         prev_sibling = LL[0].prev_sibling
1187         next_sibling = LL[0].next_sibling
1188         if not prev_sibling and not next_sibling and parent_type(LL[0]) == syms.atom:
1189             # If it's an atom string, we need to check the parent atom's siblings.
1190             parent = LL[0].parent
1191             assert parent is not None  # For type checkers.
1192             prev_sibling = parent.prev_sibling
1193             next_sibling = parent.next_sibling
1194         if (not prev_sibling or prev_sibling.type == token.COMMA) and (
1195             not next_sibling or next_sibling.type == token.COMMA
1196         ):
1197             return 0
1198
1199         return None
1200
1201
1202 def iter_fexpr_spans(s: str) -> Iterator[Tuple[int, int]]:
1203     """
1204     Yields spans corresponding to expressions in a given f-string.
1205     Spans are half-open ranges (left inclusive, right exclusive).
1206     Assumes the input string is a valid f-string, but will not crash if the input
1207     string is invalid.
1208     """
1209     stack: List[int] = []  # our curly paren stack
1210     i = 0
1211     while i < len(s):
1212         if s[i] == "{":
1213             # if we're in a string part of the f-string, ignore escaped curly braces
1214             if not stack and i + 1 < len(s) and s[i + 1] == "{":
1215                 i += 2
1216                 continue
1217             stack.append(i)
1218             i += 1
1219             continue
1220
1221         if s[i] == "}":
1222             if not stack:
1223                 i += 1
1224                 continue
1225             j = stack.pop()
1226             # we've made it back out of the expression! yield the span
1227             if not stack:
1228                 yield (j, i + 1)
1229             i += 1
1230             continue
1231
1232         # if we're in an expression part of the f-string, fast forward through strings
1233         # note that backslashes are not legal in the expression portion of f-strings
1234         if stack:
1235             delim = None
1236             if s[i : i + 3] in ("'''", '"""'):
1237                 delim = s[i : i + 3]
1238             elif s[i] in ("'", '"'):
1239                 delim = s[i]
1240             if delim:
1241                 i += len(delim)
1242                 while i < len(s) and s[i : i + len(delim)] != delim:
1243                     i += 1
1244                 i += len(delim)
1245                 continue
1246         i += 1
1247
1248
1249 def fstring_contains_expr(s: str) -> bool:
1250     return any(iter_fexpr_spans(s))
1251
1252
1253 def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str:
1254     """
1255     Toggles quotes used in f-string expressions that are `old_quote`.
1256
1257     f-string expressions can't contain backslashes, so we need to toggle the
1258     quotes if the f-string itself will end up using the same quote. We can
1259     simply toggle without escaping because, quotes can't be reused in f-string
1260     expressions. They will fail to parse.
1261
1262     NOTE: If PEP 701 is accepted, above statement will no longer be true.
1263     Though if quotes can be reused, we can simply reuse them without updates or
1264     escaping, once Black figures out how to parse the new grammar.
1265     """
1266     new_quote = "'" if old_quote == '"' else '"'
1267     parts = []
1268     previous_index = 0
1269     for start, end in iter_fexpr_spans(fstring):
1270         parts.append(fstring[previous_index:start])
1271         parts.append(fstring[start:end].replace(old_quote, new_quote))
1272         previous_index = end
1273     parts.append(fstring[previous_index:])
1274     return "".join(parts)
1275
1276
1277 class StringSplitter(BaseStringSplitter, CustomSplitMapMixin):
1278     """
1279     StringTransformer that splits "atom" strings (i.e. strings which exist on
1280     lines by themselves).
1281
1282     Requirements:
1283         * The line consists ONLY of a single string (possibly prefixed by a
1284         string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
1285         a trailing comma.
1286             AND
1287         * All of the requirements listed in BaseStringSplitter's docstring.
1288
1289     Transformations:
1290         The string mentioned in the 'Requirements' section is split into as
1291         many substrings as necessary to adhere to the configured line length.
1292
1293         In the final set of substrings, no substring should be smaller than
1294         MIN_SUBSTR_SIZE characters.
1295
1296         The string will ONLY be split on spaces (i.e. each new substring should
1297         start with a space). Note that the string will NOT be split on a space
1298         which is escaped with a backslash.
1299
1300         If the string is an f-string, it will NOT be split in the middle of an
1301         f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
1302         else bar()} is an f-expression).
1303
1304         If the string that is being split has an associated set of custom split
1305         records and those custom splits will NOT result in any line going over
1306         the configured line length, those custom splits are used. Otherwise the
1307         string is split as late as possible (from left-to-right) while still
1308         adhering to the transformation rules listed above.
1309
1310     Collaborations:
1311         StringSplitter relies on StringMerger to construct the appropriate
1312         CustomSplit objects and add them to the custom split map.
1313     """
1314
1315     MIN_SUBSTR_SIZE: Final = 6
1316
1317     def do_splitter_match(self, line: Line) -> TMatchResult:
1318         LL = line.leaves
1319
1320         if self._prefer_paren_wrap_match(LL) is not None:
1321             return TErr("Line needs to be wrapped in parens first.")
1322
1323         is_valid_index = is_valid_index_factory(LL)
1324
1325         idx = 0
1326
1327         # The first two leaves MAY be the 'not in' keywords...
1328         if (
1329             is_valid_index(idx)
1330             and is_valid_index(idx + 1)
1331             and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
1332             and str(LL[idx]) + str(LL[idx + 1]) == "not in"
1333         ):
1334             idx += 2
1335         # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
1336         elif is_valid_index(idx) and (
1337             LL[idx].type in self.STRING_OPERATORS
1338             or LL[idx].type == token.NAME
1339             and str(LL[idx]) == "in"
1340         ):
1341             idx += 1
1342
1343         # The next/first leaf MAY be an empty LPAR...
1344         if is_valid_index(idx) and is_empty_lpar(LL[idx]):
1345             idx += 1
1346
1347         # The next/first leaf MUST be a string...
1348         if not is_valid_index(idx) or LL[idx].type != token.STRING:
1349             return TErr("Line does not start with a string.")
1350
1351         string_idx = idx
1352
1353         # Skip the string trailer, if one exists.
1354         string_parser = StringParser()
1355         idx = string_parser.parse(LL, string_idx)
1356
1357         # That string MAY be followed by an empty RPAR...
1358         if is_valid_index(idx) and is_empty_rpar(LL[idx]):
1359             idx += 1
1360
1361         # That string / empty RPAR leaf MAY be followed by a comma...
1362         if is_valid_index(idx) and LL[idx].type == token.COMMA:
1363             idx += 1
1364
1365         # But no more leaves are allowed...
1366         if is_valid_index(idx):
1367             return TErr("This line does not end with a string.")
1368
1369         return Ok([string_idx])
1370
1371     def do_transform(
1372         self, line: Line, string_indices: List[int]
1373     ) -> Iterator[TResult[Line]]:
1374         LL = line.leaves
1375         assert len(string_indices) == 1, (
1376             f"{self.__class__.__name__} should only find one match at a time, found"
1377             f" {len(string_indices)}"
1378         )
1379         string_idx = string_indices[0]
1380
1381         QUOTE = LL[string_idx].value[-1]
1382
1383         is_valid_index = is_valid_index_factory(LL)
1384         insert_str_child = insert_str_child_factory(LL[string_idx])
1385
1386         prefix = get_string_prefix(LL[string_idx].value).lower()
1387
1388         # We MAY choose to drop the 'f' prefix from substrings that don't
1389         # contain any f-expressions, but ONLY if the original f-string
1390         # contains at least one f-expression. Otherwise, we will alter the AST
1391         # of the program.
1392         drop_pointless_f_prefix = ("f" in prefix) and fstring_contains_expr(
1393             LL[string_idx].value
1394         )
1395
1396         first_string_line = True
1397
1398         string_op_leaves = self._get_string_operator_leaves(LL)
1399         string_op_leaves_length = (
1400             sum(len(str(prefix_leaf)) for prefix_leaf in string_op_leaves) + 1
1401             if string_op_leaves
1402             else 0
1403         )
1404
1405         def maybe_append_string_operators(new_line: Line) -> None:
1406             """
1407             Side Effects:
1408                 If @line starts with a string operator and this is the first
1409                 line we are constructing, this function appends the string
1410                 operator to @new_line and replaces the old string operator leaf
1411                 in the node structure. Otherwise this function does nothing.
1412             """
1413             maybe_prefix_leaves = string_op_leaves if first_string_line else []
1414             for i, prefix_leaf in enumerate(maybe_prefix_leaves):
1415                 replace_child(LL[i], prefix_leaf)
1416                 new_line.append(prefix_leaf)
1417
1418         ends_with_comma = (
1419             is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1420         )
1421
1422         def max_last_string() -> int:
1423             """
1424             Returns:
1425                 The max allowed length of the string value used for the last
1426                 line we will construct.
1427             """
1428             result = self.line_length
1429             result -= line.depth * 4
1430             result -= 1 if ends_with_comma else 0
1431             result -= string_op_leaves_length
1432             return result
1433
1434         # --- Calculate Max Break Index (for string value)
1435         # We start with the line length limit
1436         max_break_idx = self.line_length
1437         # The last index of a string of length N is N-1.
1438         max_break_idx -= 1
1439         # Leading whitespace is not present in the string value (e.g. Leaf.value).
1440         max_break_idx -= line.depth * 4
1441         if max_break_idx < 0:
1442             yield TErr(
1443                 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1444                 f" {line.depth}"
1445             )
1446             return
1447
1448         # Check if StringMerger registered any custom splits.
1449         custom_splits = self.pop_custom_splits(LL[string_idx].value)
1450         # We use them ONLY if none of them would produce lines that exceed the
1451         # line limit.
1452         use_custom_breakpoints = bool(
1453             custom_splits
1454             and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
1455         )
1456
1457         # Temporary storage for the remaining chunk of the string line that
1458         # can't fit onto the line currently being constructed.
1459         rest_value = LL[string_idx].value
1460
1461         def more_splits_should_be_made() -> bool:
1462             """
1463             Returns:
1464                 True iff `rest_value` (the remaining string value from the last
1465                 split), should be split again.
1466             """
1467             if use_custom_breakpoints:
1468                 return len(custom_splits) > 1
1469             else:
1470                 return len(rest_value) > max_last_string()
1471
1472         string_line_results: List[Ok[Line]] = []
1473         while more_splits_should_be_made():
1474             if use_custom_breakpoints:
1475                 # Custom User Split (manual)
1476                 csplit = custom_splits.pop(0)
1477                 break_idx = csplit.break_idx
1478             else:
1479                 # Algorithmic Split (automatic)
1480                 max_bidx = max_break_idx - string_op_leaves_length
1481                 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
1482                 if maybe_break_idx is None:
1483                     # If we are unable to algorithmically determine a good split
1484                     # and this string has custom splits registered to it, we
1485                     # fall back to using them--which means we have to start
1486                     # over from the beginning.
1487                     if custom_splits:
1488                         rest_value = LL[string_idx].value
1489                         string_line_results = []
1490                         first_string_line = True
1491                         use_custom_breakpoints = True
1492                         continue
1493
1494                     # Otherwise, we stop splitting here.
1495                     break
1496
1497                 break_idx = maybe_break_idx
1498
1499             # --- Construct `next_value`
1500             next_value = rest_value[:break_idx] + QUOTE
1501
1502             # HACK: The following 'if' statement is a hack to fix the custom
1503             # breakpoint index in the case of either: (a) substrings that were
1504             # f-strings but will have the 'f' prefix removed OR (b) substrings
1505             # that were not f-strings but will now become f-strings because of
1506             # redundant use of the 'f' prefix (i.e. none of the substrings
1507             # contain f-expressions but one or more of them had the 'f' prefix
1508             # anyway; in which case, we will prepend 'f' to _all_ substrings).
1509             #
1510             # There is probably a better way to accomplish what is being done
1511             # here...
1512             #
1513             # If this substring is an f-string, we _could_ remove the 'f'
1514             # prefix, and the current custom split did NOT originally use a
1515             # prefix...
1516             if (
1517                 use_custom_breakpoints
1518                 and not csplit.has_prefix
1519                 and (
1520                     # `next_value == prefix + QUOTE` happens when the custom
1521                     # split is an empty string.
1522                     next_value == prefix + QUOTE
1523                     or next_value != self._normalize_f_string(next_value, prefix)
1524                 )
1525             ):
1526                 # Then `csplit.break_idx` will be off by one after removing
1527                 # the 'f' prefix.
1528                 break_idx += 1
1529                 next_value = rest_value[:break_idx] + QUOTE
1530
1531             if drop_pointless_f_prefix:
1532                 next_value = self._normalize_f_string(next_value, prefix)
1533
1534             # --- Construct `next_leaf`
1535             next_leaf = Leaf(token.STRING, next_value)
1536             insert_str_child(next_leaf)
1537             self._maybe_normalize_string_quotes(next_leaf)
1538
1539             # --- Construct `next_line`
1540             next_line = line.clone()
1541             maybe_append_string_operators(next_line)
1542             next_line.append(next_leaf)
1543             string_line_results.append(Ok(next_line))
1544
1545             rest_value = prefix + QUOTE + rest_value[break_idx:]
1546             first_string_line = False
1547
1548         yield from string_line_results
1549
1550         if drop_pointless_f_prefix:
1551             rest_value = self._normalize_f_string(rest_value, prefix)
1552
1553         rest_leaf = Leaf(token.STRING, rest_value)
1554         insert_str_child(rest_leaf)
1555
1556         # NOTE: I could not find a test case that verifies that the following
1557         # line is actually necessary, but it seems to be. Otherwise we risk
1558         # not normalizing the last substring, right?
1559         self._maybe_normalize_string_quotes(rest_leaf)
1560
1561         last_line = line.clone()
1562         maybe_append_string_operators(last_line)
1563
1564         # If there are any leaves to the right of the target string...
1565         if is_valid_index(string_idx + 1):
1566             # We use `temp_value` here to determine how long the last line
1567             # would be if we were to append all the leaves to the right of the
1568             # target string to the last string line.
1569             temp_value = rest_value
1570             for leaf in LL[string_idx + 1 :]:
1571                 temp_value += str(leaf)
1572                 if leaf.type == token.LPAR:
1573                     break
1574
1575             # Try to fit them all on the same line with the last substring...
1576             if (
1577                 len(temp_value) <= max_last_string()
1578                 or LL[string_idx + 1].type == token.COMMA
1579             ):
1580                 last_line.append(rest_leaf)
1581                 append_leaves(last_line, line, LL[string_idx + 1 :])
1582                 yield Ok(last_line)
1583             # Otherwise, place the last substring on one line and everything
1584             # else on a line below that...
1585             else:
1586                 last_line.append(rest_leaf)
1587                 yield Ok(last_line)
1588
1589                 non_string_line = line.clone()
1590                 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1591                 yield Ok(non_string_line)
1592         # Else the target string was the last leaf...
1593         else:
1594             last_line.append(rest_leaf)
1595             last_line.comments = line.comments.copy()
1596             yield Ok(last_line)
1597
1598     def _iter_nameescape_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1599         """
1600         Yields:
1601             All ranges of @string which, if @string were to be split there,
1602             would result in the splitting of an \\N{...} expression (which is NOT
1603             allowed).
1604         """
1605         # True - the previous backslash was unescaped
1606         # False - the previous backslash was escaped *or* there was no backslash
1607         previous_was_unescaped_backslash = False
1608         it = iter(enumerate(string))
1609         for idx, c in it:
1610             if c == "\\":
1611                 previous_was_unescaped_backslash = not previous_was_unescaped_backslash
1612                 continue
1613             if not previous_was_unescaped_backslash or c != "N":
1614                 previous_was_unescaped_backslash = False
1615                 continue
1616             previous_was_unescaped_backslash = False
1617
1618             begin = idx - 1  # the position of backslash before \N{...}
1619             for idx, c in it:
1620                 if c == "}":
1621                     end = idx
1622                     break
1623             else:
1624                 # malformed nameescape expression?
1625                 # should have been detected by AST parsing earlier...
1626                 raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1627             yield begin, end
1628
1629     def _iter_fexpr_slices(self, string: str) -> Iterator[Tuple[Index, Index]]:
1630         """
1631         Yields:
1632             All ranges of @string which, if @string were to be split there,
1633             would result in the splitting of an f-expression (which is NOT
1634             allowed).
1635         """
1636         if "f" not in get_string_prefix(string).lower():
1637             return
1638         yield from iter_fexpr_spans(string)
1639
1640     def _get_illegal_split_indices(self, string: str) -> Set[Index]:
1641         illegal_indices: Set[Index] = set()
1642         iterators = [
1643             self._iter_fexpr_slices(string),
1644             self._iter_nameescape_slices(string),
1645         ]
1646         for it in iterators:
1647             for begin, end in it:
1648                 illegal_indices.update(range(begin, end + 1))
1649         return illegal_indices
1650
1651     def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
1652         """
1653         This method contains the algorithm that StringSplitter uses to
1654         determine which character to split each string at.
1655
1656         Args:
1657             @string: The substring that we are attempting to split.
1658             @max_break_idx: The ideal break index. We will return this value if it
1659             meets all the necessary conditions. In the likely event that it
1660             doesn't we will try to find the closest index BELOW @max_break_idx
1661             that does. If that fails, we will expand our search by also
1662             considering all valid indices ABOVE @max_break_idx.
1663
1664         Pre-Conditions:
1665             * assert_is_leaf_string(@string)
1666             * 0 <= @max_break_idx < len(@string)
1667
1668         Returns:
1669             break_idx, if an index is able to be found that meets all of the
1670             conditions listed in the 'Transformations' section of this classes'
1671             docstring.
1672                 OR
1673             None, otherwise.
1674         """
1675         is_valid_index = is_valid_index_factory(string)
1676
1677         assert is_valid_index(max_break_idx)
1678         assert_is_leaf_string(string)
1679
1680         _illegal_split_indices = self._get_illegal_split_indices(string)
1681
1682         def breaks_unsplittable_expression(i: Index) -> bool:
1683             """
1684             Returns:
1685                 True iff returning @i would result in the splitting of an
1686                 unsplittable expression (which is NOT allowed).
1687             """
1688             return i in _illegal_split_indices
1689
1690         def passes_all_checks(i: Index) -> bool:
1691             """
1692             Returns:
1693                 True iff ALL of the conditions listed in the 'Transformations'
1694                 section of this classes' docstring would be be met by returning @i.
1695             """
1696             is_space = string[i] == " "
1697
1698             is_not_escaped = True
1699             j = i - 1
1700             while is_valid_index(j) and string[j] == "\\":
1701                 is_not_escaped = not is_not_escaped
1702                 j -= 1
1703
1704             is_big_enough = (
1705                 len(string[i:]) >= self.MIN_SUBSTR_SIZE
1706                 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
1707             )
1708             return (
1709                 is_space
1710                 and is_not_escaped
1711                 and is_big_enough
1712                 and not breaks_unsplittable_expression(i)
1713             )
1714
1715         # First, we check all indices BELOW @max_break_idx.
1716         break_idx = max_break_idx
1717         while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1718             break_idx -= 1
1719
1720         if not passes_all_checks(break_idx):
1721             # If that fails, we check all indices ABOVE @max_break_idx.
1722             #
1723             # If we are able to find a valid index here, the next line is going
1724             # to be longer than the specified line length, but it's probably
1725             # better than doing nothing at all.
1726             break_idx = max_break_idx + 1
1727             while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1728                 break_idx += 1
1729
1730             if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1731                 return None
1732
1733         return break_idx
1734
1735     def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1736         if self.normalize_strings:
1737             leaf.value = normalize_string_quotes(leaf.value)
1738
1739     def _normalize_f_string(self, string: str, prefix: str) -> str:
1740         """
1741         Pre-Conditions:
1742             * assert_is_leaf_string(@string)
1743
1744         Returns:
1745             * If @string is an f-string that contains no f-expressions, we
1746             return a string identical to @string except that the 'f' prefix
1747             has been stripped and all double braces (i.e. '{{' or '}}') have
1748             been normalized (i.e. turned into '{' or '}').
1749                 OR
1750             * Otherwise, we return @string.
1751         """
1752         assert_is_leaf_string(string)
1753
1754         if "f" in prefix and not fstring_contains_expr(string):
1755             new_prefix = prefix.replace("f", "")
1756
1757             temp = string[len(prefix) :]
1758             temp = re.sub(r"\{\{", "{", temp)
1759             temp = re.sub(r"\}\}", "}", temp)
1760             new_string = temp
1761
1762             return f"{new_prefix}{new_string}"
1763         else:
1764             return string
1765
1766     def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]:
1767         LL = list(leaves)
1768
1769         string_op_leaves = []
1770         i = 0
1771         while LL[i].type in self.STRING_OPERATORS + [token.NAME]:
1772             prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
1773             string_op_leaves.append(prefix_leaf)
1774             i += 1
1775         return string_op_leaves
1776
1777
1778 class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
1779     """
1780     StringTransformer that wraps strings in parens and then splits at the LPAR.
1781
1782     Requirements:
1783         All of the requirements listed in BaseStringSplitter's docstring in
1784         addition to the requirements listed below:
1785
1786         * The line is a return/yield statement, which returns/yields a string.
1787             OR
1788         * The line is part of a ternary expression (e.g. `x = y if cond else
1789         z`) such that the line starts with `else <string>`, where <string> is
1790         some string.
1791             OR
1792         * The line is an assert statement, which ends with a string.
1793             OR
1794         * The line is an assignment statement (e.g. `x = <string>` or `x +=
1795         <string>`) such that the variable is being assigned the value of some
1796         string.
1797             OR
1798         * The line is a dictionary key assignment where some valid key is being
1799         assigned the value of some string.
1800             OR
1801         * The line is an lambda expression and the value is a string.
1802             OR
1803         * The line starts with an "atom" string that prefers to be wrapped in
1804         parens. It's preferred to be wrapped when the string is surrounded by
1805         commas (or is the first/last child).
1806
1807     Transformations:
1808         The chosen string is wrapped in parentheses and then split at the LPAR.
1809
1810         We then have one line which ends with an LPAR and another line that
1811         starts with the chosen string. The latter line is then split again at
1812         the RPAR. This results in the RPAR (and possibly a trailing comma)
1813         being placed on its own line.
1814
1815         NOTE: If any leaves exist to the right of the chosen string (except
1816         for a trailing comma, which would be placed after the RPAR), those
1817         leaves are placed inside the parentheses.  In effect, the chosen
1818         string is not necessarily being "wrapped" by parentheses. We can,
1819         however, count on the LPAR being placed directly before the chosen
1820         string.
1821
1822         In other words, StringParenWrapper creates "atom" strings. These
1823         can then be split again by StringSplitter, if necessary.
1824
1825     Collaborations:
1826         In the event that a string line split by StringParenWrapper is
1827         changed such that it no longer needs to be given its own line,
1828         StringParenWrapper relies on StringParenStripper to clean up the
1829         parentheses it created.
1830
1831         For "atom" strings that prefers to be wrapped in parens, it requires
1832         StringSplitter to hold the split until the string is wrapped in parens.
1833     """
1834
1835     def do_splitter_match(self, line: Line) -> TMatchResult:
1836         LL = line.leaves
1837
1838         if line.leaves[-1].type in OPENING_BRACKETS:
1839             return TErr(
1840                 "Cannot wrap parens around a line that ends in an opening bracket."
1841             )
1842
1843         string_idx = (
1844             self._return_match(LL)
1845             or self._else_match(LL)
1846             or self._assert_match(LL)
1847             or self._assign_match(LL)
1848             or self._dict_or_lambda_match(LL)
1849             or self._prefer_paren_wrap_match(LL)
1850         )
1851
1852         if string_idx is not None:
1853             string_value = line.leaves[string_idx].value
1854             # If the string has no spaces...
1855             if " " not in string_value:
1856                 # And will still violate the line length limit when split...
1857                 max_string_length = self.line_length - ((line.depth + 1) * 4)
1858                 if len(string_value) > max_string_length:
1859                     # And has no associated custom splits...
1860                     if not self.has_custom_splits(string_value):
1861                         # Then we should NOT put this string on its own line.
1862                         return TErr(
1863                             "We do not wrap long strings in parentheses when the"
1864                             " resultant line would still be over the specified line"
1865                             " length and can't be split further by StringSplitter."
1866                         )
1867             return Ok([string_idx])
1868
1869         return TErr("This line does not contain any non-atomic strings.")
1870
1871     @staticmethod
1872     def _return_match(LL: List[Leaf]) -> Optional[int]:
1873         """
1874         Returns:
1875             string_idx such that @LL[string_idx] is equal to our target (i.e.
1876             matched) string, if this line matches the return/yield statement
1877             requirements listed in the 'Requirements' section of this classes'
1878             docstring.
1879                 OR
1880             None, otherwise.
1881         """
1882         # If this line is apart of a return/yield statement and the first leaf
1883         # contains either the "return" or "yield" keywords...
1884         if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
1885             0
1886         ].value in ["return", "yield"]:
1887             is_valid_index = is_valid_index_factory(LL)
1888
1889             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1890             # The next visible leaf MUST contain a string...
1891             if is_valid_index(idx) and LL[idx].type == token.STRING:
1892                 return idx
1893
1894         return None
1895
1896     @staticmethod
1897     def _else_match(LL: List[Leaf]) -> Optional[int]:
1898         """
1899         Returns:
1900             string_idx such that @LL[string_idx] is equal to our target (i.e.
1901             matched) string, if this line matches the ternary expression
1902             requirements listed in the 'Requirements' section of this classes'
1903             docstring.
1904                 OR
1905             None, otherwise.
1906         """
1907         # If this line is apart of a ternary expression and the first leaf
1908         # contains the "else" keyword...
1909         if (
1910             parent_type(LL[0]) == syms.test
1911             and LL[0].type == token.NAME
1912             and LL[0].value == "else"
1913         ):
1914             is_valid_index = is_valid_index_factory(LL)
1915
1916             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1917             # The next visible leaf MUST contain a string...
1918             if is_valid_index(idx) and LL[idx].type == token.STRING:
1919                 return idx
1920
1921         return None
1922
1923     @staticmethod
1924     def _assert_match(LL: List[Leaf]) -> Optional[int]:
1925         """
1926         Returns:
1927             string_idx such that @LL[string_idx] is equal to our target (i.e.
1928             matched) string, if this line matches the assert statement
1929             requirements listed in the 'Requirements' section of this classes'
1930             docstring.
1931                 OR
1932             None, otherwise.
1933         """
1934         # If this line is apart of an assert statement and the first leaf
1935         # contains the "assert" keyword...
1936         if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
1937             is_valid_index = is_valid_index_factory(LL)
1938
1939             for i, leaf in enumerate(LL):
1940                 # We MUST find a comma...
1941                 if leaf.type == token.COMMA:
1942                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1943
1944                     # That comma MUST be followed by a string...
1945                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1946                         string_idx = idx
1947
1948                         # Skip the string trailer, if one exists.
1949                         string_parser = StringParser()
1950                         idx = string_parser.parse(LL, string_idx)
1951
1952                         # But no more leaves are allowed...
1953                         if not is_valid_index(idx):
1954                             return string_idx
1955
1956         return None
1957
1958     @staticmethod
1959     def _assign_match(LL: List[Leaf]) -> Optional[int]:
1960         """
1961         Returns:
1962             string_idx such that @LL[string_idx] is equal to our target (i.e.
1963             matched) string, if this line matches the assignment statement
1964             requirements listed in the 'Requirements' section of this classes'
1965             docstring.
1966                 OR
1967             None, otherwise.
1968         """
1969         # If this line is apart of an expression statement or is a function
1970         # argument AND the first leaf contains a variable name...
1971         if (
1972             parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
1973             and LL[0].type == token.NAME
1974         ):
1975             is_valid_index = is_valid_index_factory(LL)
1976
1977             for i, leaf in enumerate(LL):
1978                 # We MUST find either an '=' or '+=' symbol...
1979                 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
1980                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1981
1982                     # That symbol MUST be followed by a string...
1983                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1984                         string_idx = idx
1985
1986                         # Skip the string trailer, if one exists.
1987                         string_parser = StringParser()
1988                         idx = string_parser.parse(LL, string_idx)
1989
1990                         # The next leaf MAY be a comma iff this line is apart
1991                         # of a function argument...
1992                         if (
1993                             parent_type(LL[0]) == syms.argument
1994                             and is_valid_index(idx)
1995                             and LL[idx].type == token.COMMA
1996                         ):
1997                             idx += 1
1998
1999                         # But no more leaves are allowed...
2000                         if not is_valid_index(idx):
2001                             return string_idx
2002
2003         return None
2004
2005     @staticmethod
2006     def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]:
2007         """
2008         Returns:
2009             string_idx such that @LL[string_idx] is equal to our target (i.e.
2010             matched) string, if this line matches the dictionary key assignment
2011             statement or lambda expression requirements listed in the
2012             'Requirements' section of this classes' docstring.
2013                 OR
2014             None, otherwise.
2015         """
2016         # If this line is a part of a dictionary key assignment or lambda expression...
2017         parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)]
2018         if syms.dictsetmaker in parent_types or syms.lambdef in parent_types:
2019             is_valid_index = is_valid_index_factory(LL)
2020
2021             for i, leaf in enumerate(LL):
2022                 # We MUST find a colon, it can either be dict's or lambda's colon...
2023                 if leaf.type == token.COLON and i < len(LL) - 1:
2024                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
2025
2026                     # That colon MUST be followed by a string...
2027                     if is_valid_index(idx) and LL[idx].type == token.STRING:
2028                         string_idx = idx
2029
2030                         # Skip the string trailer, if one exists.
2031                         string_parser = StringParser()
2032                         idx = string_parser.parse(LL, string_idx)
2033
2034                         # That string MAY be followed by a comma...
2035                         if is_valid_index(idx) and LL[idx].type == token.COMMA:
2036                             idx += 1
2037
2038                         # But no more leaves are allowed...
2039                         if not is_valid_index(idx):
2040                             return string_idx
2041
2042         return None
2043
2044     def do_transform(
2045         self, line: Line, string_indices: List[int]
2046     ) -> Iterator[TResult[Line]]:
2047         LL = line.leaves
2048         assert len(string_indices) == 1, (
2049             f"{self.__class__.__name__} should only find one match at a time, found"
2050             f" {len(string_indices)}"
2051         )
2052         string_idx = string_indices[0]
2053
2054         is_valid_index = is_valid_index_factory(LL)
2055         insert_str_child = insert_str_child_factory(LL[string_idx])
2056
2057         comma_idx = -1
2058         ends_with_comma = False
2059         if LL[comma_idx].type == token.COMMA:
2060             ends_with_comma = True
2061
2062         leaves_to_steal_comments_from = [LL[string_idx]]
2063         if ends_with_comma:
2064             leaves_to_steal_comments_from.append(LL[comma_idx])
2065
2066         # --- First Line
2067         first_line = line.clone()
2068         left_leaves = LL[:string_idx]
2069
2070         # We have to remember to account for (possibly invisible) LPAR and RPAR
2071         # leaves that already wrapped the target string. If these leaves do
2072         # exist, we will replace them with our own LPAR and RPAR leaves.
2073         old_parens_exist = False
2074         if left_leaves and left_leaves[-1].type == token.LPAR:
2075             old_parens_exist = True
2076             leaves_to_steal_comments_from.append(left_leaves[-1])
2077             left_leaves.pop()
2078
2079         append_leaves(first_line, line, left_leaves)
2080
2081         lpar_leaf = Leaf(token.LPAR, "(")
2082         if old_parens_exist:
2083             replace_child(LL[string_idx - 1], lpar_leaf)
2084         else:
2085             insert_str_child(lpar_leaf)
2086         first_line.append(lpar_leaf)
2087
2088         # We throw inline comments that were originally to the right of the
2089         # target string to the top line. They will now be shown to the right of
2090         # the LPAR.
2091         for leaf in leaves_to_steal_comments_from:
2092             for comment_leaf in line.comments_after(leaf):
2093                 first_line.append(comment_leaf, preformatted=True)
2094
2095         yield Ok(first_line)
2096
2097         # --- Middle (String) Line
2098         # We only need to yield one (possibly too long) string line, since the
2099         # `StringSplitter` will break it down further if necessary.
2100         string_value = LL[string_idx].value
2101         string_line = Line(
2102             mode=line.mode,
2103             depth=line.depth + 1,
2104             inside_brackets=True,
2105             should_split_rhs=line.should_split_rhs,
2106             magic_trailing_comma=line.magic_trailing_comma,
2107         )
2108         string_leaf = Leaf(token.STRING, string_value)
2109         insert_str_child(string_leaf)
2110         string_line.append(string_leaf)
2111
2112         old_rpar_leaf = None
2113         if is_valid_index(string_idx + 1):
2114             right_leaves = LL[string_idx + 1 :]
2115             if ends_with_comma:
2116                 right_leaves.pop()
2117
2118             if old_parens_exist:
2119                 assert right_leaves and right_leaves[-1].type == token.RPAR, (
2120                     "Apparently, old parentheses do NOT exist?!"
2121                     f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
2122                 )
2123                 old_rpar_leaf = right_leaves.pop()
2124             elif right_leaves and right_leaves[-1].type == token.RPAR:
2125                 # Special case for lambda expressions as dict's value, e.g.:
2126                 #     my_dict = {
2127                 #        "key": lambda x: f"formatted: {x},
2128                 #     }
2129                 # After wrapping the dict's value with parentheses, the string is
2130                 # followed by a RPAR but its opening bracket is lambda's, not
2131                 # the string's:
2132                 #        "key": (lambda x: f"formatted: {x}),
2133                 opening_bracket = right_leaves[-1].opening_bracket
2134                 if opening_bracket is not None and opening_bracket in left_leaves:
2135                     index = left_leaves.index(opening_bracket)
2136                     if (
2137                         index > 0
2138                         and index < len(left_leaves) - 1
2139                         and left_leaves[index - 1].type == token.COLON
2140                         and left_leaves[index + 1].value == "lambda"
2141                     ):
2142                         right_leaves.pop()
2143
2144             append_leaves(string_line, line, right_leaves)
2145
2146         yield Ok(string_line)
2147
2148         # --- Last Line
2149         last_line = line.clone()
2150         last_line.bracket_tracker = first_line.bracket_tracker
2151
2152         new_rpar_leaf = Leaf(token.RPAR, ")")
2153         if old_rpar_leaf is not None:
2154             replace_child(old_rpar_leaf, new_rpar_leaf)
2155         else:
2156             insert_str_child(new_rpar_leaf)
2157         last_line.append(new_rpar_leaf)
2158
2159         # If the target string ended with a comma, we place this comma to the
2160         # right of the RPAR on the last line.
2161         if ends_with_comma:
2162             comma_leaf = Leaf(token.COMMA, ",")
2163             replace_child(LL[comma_idx], comma_leaf)
2164             last_line.append(comma_leaf)
2165
2166         yield Ok(last_line)
2167
2168
2169 class StringParser:
2170     """
2171     A state machine that aids in parsing a string's "trailer", which can be
2172     either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
2173     (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
2174     varY)`).
2175
2176     NOTE: A new StringParser object MUST be instantiated for each string
2177     trailer we need to parse.
2178
2179     Examples:
2180         We shall assume that `line` equals the `Line` object that corresponds
2181         to the following line of python code:
2182         ```
2183         x = "Some {}.".format("String") + some_other_string
2184         ```
2185
2186         Furthermore, we will assume that `string_idx` is some index such that:
2187         ```
2188         assert line.leaves[string_idx].value == "Some {}."
2189         ```
2190
2191         The following code snippet then holds:
2192         ```
2193         string_parser = StringParser()
2194         idx = string_parser.parse(line.leaves, string_idx)
2195         assert line.leaves[idx].type == token.PLUS
2196         ```
2197     """
2198
2199     DEFAULT_TOKEN: Final = 20210605
2200
2201     # String Parser States
2202     START: Final = 1
2203     DOT: Final = 2
2204     NAME: Final = 3
2205     PERCENT: Final = 4
2206     SINGLE_FMT_ARG: Final = 5
2207     LPAR: Final = 6
2208     RPAR: Final = 7
2209     DONE: Final = 8
2210
2211     # Lookup Table for Next State
2212     _goto: Final[Dict[Tuple[ParserState, NodeType], ParserState]] = {
2213         # A string trailer may start with '.' OR '%'.
2214         (START, token.DOT): DOT,
2215         (START, token.PERCENT): PERCENT,
2216         (START, DEFAULT_TOKEN): DONE,
2217         # A '.' MUST be followed by an attribute or method name.
2218         (DOT, token.NAME): NAME,
2219         # A method name MUST be followed by an '(', whereas an attribute name
2220         # is the last symbol in the string trailer.
2221         (NAME, token.LPAR): LPAR,
2222         (NAME, DEFAULT_TOKEN): DONE,
2223         # A '%' symbol can be followed by an '(' or a single argument (e.g. a
2224         # string or variable name).
2225         (PERCENT, token.LPAR): LPAR,
2226         (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
2227         # If a '%' symbol is followed by a single argument, that argument is
2228         # the last leaf in the string trailer.
2229         (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
2230         # If present, a ')' symbol is the last symbol in a string trailer.
2231         # (NOTE: LPARS and nested RPARS are not included in this lookup table,
2232         # since they are treated as a special case by the parsing logic in this
2233         # classes' implementation.)
2234         (RPAR, DEFAULT_TOKEN): DONE,
2235     }
2236
2237     def __init__(self) -> None:
2238         self._state = self.START
2239         self._unmatched_lpars = 0
2240
2241     def parse(self, leaves: List[Leaf], string_idx: int) -> int:
2242         """
2243         Pre-conditions:
2244             * @leaves[@string_idx].type == token.STRING
2245
2246         Returns:
2247             The index directly after the last leaf which is apart of the string
2248             trailer, if a "trailer" exists.
2249                 OR
2250             @string_idx + 1, if no string "trailer" exists.
2251         """
2252         assert leaves[string_idx].type == token.STRING
2253
2254         idx = string_idx + 1
2255         while idx < len(leaves) and self._next_state(leaves[idx]):
2256             idx += 1
2257         return idx
2258
2259     def _next_state(self, leaf: Leaf) -> bool:
2260         """
2261         Pre-conditions:
2262             * On the first call to this function, @leaf MUST be the leaf that
2263             was directly after the string leaf in question (e.g. if our target
2264             string is `line.leaves[i]` then the first call to this method must
2265             be `line.leaves[i + 1]`).
2266             * On the next call to this function, the leaf parameter passed in
2267             MUST be the leaf directly following @leaf.
2268
2269         Returns:
2270             True iff @leaf is apart of the string's trailer.
2271         """
2272         # We ignore empty LPAR or RPAR leaves.
2273         if is_empty_par(leaf):
2274             return True
2275
2276         next_token = leaf.type
2277         if next_token == token.LPAR:
2278             self._unmatched_lpars += 1
2279
2280         current_state = self._state
2281
2282         # The LPAR parser state is a special case. We will return True until we
2283         # find the matching RPAR token.
2284         if current_state == self.LPAR:
2285             if next_token == token.RPAR:
2286                 self._unmatched_lpars -= 1
2287                 if self._unmatched_lpars == 0:
2288                     self._state = self.RPAR
2289         # Otherwise, we use a lookup table to determine the next state.
2290         else:
2291             # If the lookup table matches the current state to the next
2292             # token, we use the lookup table.
2293             if (current_state, next_token) in self._goto:
2294                 self._state = self._goto[current_state, next_token]
2295             else:
2296                 # Otherwise, we check if a the current state was assigned a
2297                 # default.
2298                 if (current_state, self.DEFAULT_TOKEN) in self._goto:
2299                     self._state = self._goto[current_state, self.DEFAULT_TOKEN]
2300                 # If no default has been assigned, then this parser has a logic
2301                 # error.
2302                 else:
2303                     raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
2304
2305             if self._state == self.DONE:
2306                 return False
2307
2308         return True
2309
2310
2311 def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
2312     """
2313     Factory for a convenience function that is used to orphan @string_leaf
2314     and then insert multiple new leaves into the same part of the node
2315     structure that @string_leaf had originally occupied.
2316
2317     Examples:
2318         Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
2319         string_leaf.parent`. Assume the node `N` has the following
2320         original structure:
2321
2322         Node(
2323             expr_stmt, [
2324                 Leaf(NAME, 'x'),
2325                 Leaf(EQUAL, '='),
2326                 Leaf(STRING, '"foo"'),
2327             ]
2328         )
2329
2330         We then run the code snippet shown below.
2331         ```
2332         insert_str_child = insert_str_child_factory(string_leaf)
2333
2334         lpar = Leaf(token.LPAR, '(')
2335         insert_str_child(lpar)
2336
2337         bar = Leaf(token.STRING, '"bar"')
2338         insert_str_child(bar)
2339
2340         rpar = Leaf(token.RPAR, ')')
2341         insert_str_child(rpar)
2342         ```
2343
2344         After which point, it follows that `string_leaf.parent is None` and
2345         the node `N` now has the following structure:
2346
2347         Node(
2348             expr_stmt, [
2349                 Leaf(NAME, 'x'),
2350                 Leaf(EQUAL, '='),
2351                 Leaf(LPAR, '('),
2352                 Leaf(STRING, '"bar"'),
2353                 Leaf(RPAR, ')'),
2354             ]
2355         )
2356     """
2357     string_parent = string_leaf.parent
2358     string_child_idx = string_leaf.remove()
2359
2360     def insert_str_child(child: LN) -> None:
2361         nonlocal string_child_idx
2362
2363         assert string_parent is not None
2364         assert string_child_idx is not None
2365
2366         string_parent.insert_child(string_child_idx, child)
2367         string_child_idx += 1
2368
2369     return insert_str_child
2370
2371
2372 def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
2373     """
2374     Examples:
2375         ```
2376         my_list = [1, 2, 3]
2377
2378         is_valid_index = is_valid_index_factory(my_list)
2379
2380         assert is_valid_index(0)
2381         assert is_valid_index(2)
2382
2383         assert not is_valid_index(3)
2384         assert not is_valid_index(-1)
2385         ```
2386     """
2387
2388     def is_valid_index(idx: int) -> bool:
2389         """
2390         Returns:
2391             True iff @idx is positive AND seq[@idx] does NOT raise an
2392             IndexError.
2393         """
2394         return 0 <= idx < len(seq)
2395
2396     return is_valid_index