]> 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:

Fix --experiemental-string-processing crash when matching parens not found (#2283)
[etc/vim.git] / src / black / trans.py
1 """
2 String transformers that can split and merge strings.
3 """
4 from abc import ABC, abstractmethod
5 from collections import defaultdict
6 from dataclasses import dataclass
7 import regex as re
8 from typing import (
9     Any,
10     Callable,
11     Collection,
12     Dict,
13     Iterable,
14     Iterator,
15     List,
16     Optional,
17     Sequence,
18     Tuple,
19     TypeVar,
20     Union,
21 )
22
23 from black.rusty import Result, Ok, Err
24
25 from black.mode import Feature
26 from black.nodes import syms, replace_child, parent_type
27 from black.nodes import is_empty_par, is_empty_lpar, is_empty_rpar
28 from black.nodes import OPENING_BRACKETS, CLOSING_BRACKETS, STANDALONE_COMMENT
29 from black.lines import Line, append_leaves
30 from black.brackets import BracketMatchError
31 from black.comments import contains_pragma_comment
32 from black.strings import has_triple_quotes, get_string_prefix, assert_is_leaf_string
33 from black.strings import normalize_string_quotes
34
35 from blib2to3.pytree import Leaf, Node
36 from blib2to3.pgen2 import token
37
38
39 class CannotTransform(Exception):
40     """Base class for errors raised by Transformers."""
41
42
43 # types
44 T = TypeVar("T")
45 LN = Union[Leaf, Node]
46 Transformer = Callable[[Line, Collection[Feature]], Iterator[Line]]
47 Index = int
48 NodeType = int
49 ParserState = int
50 StringID = int
51 TResult = Result[T, CannotTransform]  # (T)ransform Result
52 TMatchResult = TResult[Index]
53
54
55 def TErr(err_msg: str) -> Err[CannotTransform]:
56     """(T)ransform Err
57
58     Convenience function used when working with the TResult type.
59     """
60     cant_transform = CannotTransform(err_msg)
61     return Err(cant_transform)
62
63
64 @dataclass  # type: ignore
65 class StringTransformer(ABC):
66     """
67     An implementation of the Transformer protocol that relies on its
68     subclasses overriding the template methods `do_match(...)` and
69     `do_transform(...)`.
70
71     This Transformer works exclusively on strings (for example, by merging
72     or splitting them).
73
74     The following sections can be found among the docstrings of each concrete
75     StringTransformer subclass.
76
77     Requirements:
78         Which requirements must be met of the given Line for this
79         StringTransformer to be applied?
80
81     Transformations:
82         If the given Line meets all of the above requirements, which string
83         transformations can you expect to be applied to it by this
84         StringTransformer?
85
86     Collaborations:
87         What contractual agreements does this StringTransformer have with other
88         StringTransfomers? Such collaborations should be eliminated/minimized
89         as much as possible.
90     """
91
92     line_length: int
93     normalize_strings: bool
94     __name__ = "StringTransformer"
95
96     @abstractmethod
97     def do_match(self, line: Line) -> TMatchResult:
98         """
99         Returns:
100             * Ok(string_idx) such that `line.leaves[string_idx]` is our target
101             string, if a match was able to be made.
102                 OR
103             * Err(CannotTransform), if a match was not able to be made.
104         """
105
106     @abstractmethod
107     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
108         """
109         Yields:
110             * Ok(new_line) where new_line is the new transformed line.
111                 OR
112             * Err(CannotTransform) if the transformation failed for some reason. The
113             `do_match(...)` template method should usually be used to reject
114             the form of the given Line, but in some cases it is difficult to
115             know whether or not a Line meets the StringTransformer's
116             requirements until the transformation is already midway.
117
118         Side Effects:
119             This method should NOT mutate @line directly, but it MAY mutate the
120             Line's underlying Node structure. (WARNING: If the underlying Node
121             structure IS altered, then this method should NOT be allowed to
122             yield an CannotTransform after that point.)
123         """
124
125     def __call__(self, line: Line, _features: Collection[Feature]) -> Iterator[Line]:
126         """
127         StringTransformer instances have a call signature that mirrors that of
128         the Transformer type.
129
130         Raises:
131             CannotTransform(...) if the concrete StringTransformer class is unable
132             to transform @line.
133         """
134         # Optimization to avoid calling `self.do_match(...)` when the line does
135         # not contain any string.
136         if not any(leaf.type == token.STRING for leaf in line.leaves):
137             raise CannotTransform("There are no strings in this line.")
138
139         match_result = self.do_match(line)
140
141         if isinstance(match_result, Err):
142             cant_transform = match_result.err()
143             raise CannotTransform(
144                 f"The string transformer {self.__class__.__name__} does not recognize"
145                 " this line as one that it can transform."
146             ) from cant_transform
147
148         string_idx = match_result.ok()
149
150         for line_result in self.do_transform(line, string_idx):
151             if isinstance(line_result, Err):
152                 cant_transform = line_result.err()
153                 raise CannotTransform(
154                     "StringTransformer failed while attempting to transform string."
155                 ) from cant_transform
156             line = line_result.ok()
157             yield line
158
159
160 @dataclass
161 class CustomSplit:
162     """A custom (i.e. manual) string split.
163
164     A single CustomSplit instance represents a single substring.
165
166     Examples:
167         Consider the following string:
168         ```
169         "Hi there friend."
170         " This is a custom"
171         f" string {split}."
172         ```
173
174         This string will correspond to the following three CustomSplit instances:
175         ```
176         CustomSplit(False, 16)
177         CustomSplit(False, 17)
178         CustomSplit(True, 16)
179         ```
180     """
181
182     has_prefix: bool
183     break_idx: int
184
185
186 class CustomSplitMapMixin:
187     """
188     This mixin class is used to map merged strings to a sequence of
189     CustomSplits, which will then be used to re-split the strings iff none of
190     the resultant substrings go over the configured max line length.
191     """
192
193     _Key = Tuple[StringID, str]
194     _CUSTOM_SPLIT_MAP: Dict[_Key, Tuple[CustomSplit, ...]] = defaultdict(tuple)
195
196     @staticmethod
197     def _get_key(string: str) -> "CustomSplitMapMixin._Key":
198         """
199         Returns:
200             A unique identifier that is used internally to map @string to a
201             group of custom splits.
202         """
203         return (id(string), string)
204
205     def add_custom_splits(
206         self, string: str, custom_splits: Iterable[CustomSplit]
207     ) -> None:
208         """Custom Split Map Setter Method
209
210         Side Effects:
211             Adds a mapping from @string to the custom splits @custom_splits.
212         """
213         key = self._get_key(string)
214         self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits)
215
216     def pop_custom_splits(self, string: str) -> List[CustomSplit]:
217         """Custom Split Map Getter Method
218
219         Returns:
220             * A list of the custom splits that are mapped to @string, if any
221             exist.
222                 OR
223             * [], otherwise.
224
225         Side Effects:
226             Deletes the mapping between @string and its associated custom
227             splits (which are returned to the caller).
228         """
229         key = self._get_key(string)
230
231         custom_splits = self._CUSTOM_SPLIT_MAP[key]
232         del self._CUSTOM_SPLIT_MAP[key]
233
234         return list(custom_splits)
235
236     def has_custom_splits(self, string: str) -> bool:
237         """
238         Returns:
239             True iff @string is associated with a set of custom splits.
240         """
241         key = self._get_key(string)
242         return key in self._CUSTOM_SPLIT_MAP
243
244
245 class StringMerger(CustomSplitMapMixin, StringTransformer):
246     """StringTransformer that merges strings together.
247
248     Requirements:
249         (A) The line contains adjacent strings such that ALL of the validation checks
250         listed in StringMerger.__validate_msg(...)'s docstring pass.
251             OR
252         (B) The line contains a string which uses line continuation backslashes.
253
254     Transformations:
255         Depending on which of the two requirements above where met, either:
256
257         (A) The string group associated with the target string is merged.
258             OR
259         (B) All line-continuation backslashes are removed from the target string.
260
261     Collaborations:
262         StringMerger provides custom split information to StringSplitter.
263     """
264
265     def do_match(self, line: Line) -> TMatchResult:
266         LL = line.leaves
267
268         is_valid_index = is_valid_index_factory(LL)
269
270         for (i, leaf) in enumerate(LL):
271             if (
272                 leaf.type == token.STRING
273                 and is_valid_index(i + 1)
274                 and LL[i + 1].type == token.STRING
275             ):
276                 return Ok(i)
277
278             if leaf.type == token.STRING and "\\\n" in leaf.value:
279                 return Ok(i)
280
281         return TErr("This line has no strings that need merging.")
282
283     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
284         new_line = line
285         rblc_result = self._remove_backslash_line_continuation_chars(
286             new_line, string_idx
287         )
288         if isinstance(rblc_result, Ok):
289             new_line = rblc_result.ok()
290
291         msg_result = self._merge_string_group(new_line, string_idx)
292         if isinstance(msg_result, Ok):
293             new_line = msg_result.ok()
294
295         if isinstance(rblc_result, Err) and isinstance(msg_result, Err):
296             msg_cant_transform = msg_result.err()
297             rblc_cant_transform = rblc_result.err()
298             cant_transform = CannotTransform(
299                 "StringMerger failed to merge any strings in this line."
300             )
301
302             # Chain the errors together using `__cause__`.
303             msg_cant_transform.__cause__ = rblc_cant_transform
304             cant_transform.__cause__ = msg_cant_transform
305
306             yield Err(cant_transform)
307         else:
308             yield Ok(new_line)
309
310     @staticmethod
311     def _remove_backslash_line_continuation_chars(
312         line: Line, string_idx: int
313     ) -> TResult[Line]:
314         """
315         Merge strings that were split across multiple lines using
316         line-continuation backslashes.
317
318         Returns:
319             Ok(new_line), if @line contains backslash line-continuation
320             characters.
321                 OR
322             Err(CannotTransform), otherwise.
323         """
324         LL = line.leaves
325
326         string_leaf = LL[string_idx]
327         if not (
328             string_leaf.type == token.STRING
329             and "\\\n" in string_leaf.value
330             and not has_triple_quotes(string_leaf.value)
331         ):
332             return TErr(
333                 f"String leaf {string_leaf} does not contain any backslash line"
334                 " continuation characters."
335             )
336
337         new_line = line.clone()
338         new_line.comments = line.comments.copy()
339         append_leaves(new_line, line, LL)
340
341         new_string_leaf = new_line.leaves[string_idx]
342         new_string_leaf.value = new_string_leaf.value.replace("\\\n", "")
343
344         return Ok(new_line)
345
346     def _merge_string_group(self, line: Line, string_idx: int) -> TResult[Line]:
347         """
348         Merges string group (i.e. set of adjacent strings) where the first
349         string in the group is `line.leaves[string_idx]`.
350
351         Returns:
352             Ok(new_line), if ALL of the validation checks found in
353             __validate_msg(...) pass.
354                 OR
355             Err(CannotTransform), otherwise.
356         """
357         LL = line.leaves
358
359         is_valid_index = is_valid_index_factory(LL)
360
361         vresult = self._validate_msg(line, string_idx)
362         if isinstance(vresult, Err):
363             return vresult
364
365         # If the string group is wrapped inside an Atom node, we must make sure
366         # to later replace that Atom with our new (merged) string leaf.
367         atom_node = LL[string_idx].parent
368
369         # We will place BREAK_MARK in between every two substrings that we
370         # merge. We will then later go through our final result and use the
371         # various instances of BREAK_MARK we find to add the right values to
372         # the custom split map.
373         BREAK_MARK = "@@@@@ BLACK BREAKPOINT MARKER @@@@@"
374
375         QUOTE = LL[string_idx].value[-1]
376
377         def make_naked(string: str, string_prefix: str) -> str:
378             """Strip @string (i.e. make it a "naked" string)
379
380             Pre-conditions:
381                 * assert_is_leaf_string(@string)
382
383             Returns:
384                 A string that is identical to @string except that
385                 @string_prefix has been stripped, the surrounding QUOTE
386                 characters have been removed, and any remaining QUOTE
387                 characters have been escaped.
388             """
389             assert_is_leaf_string(string)
390
391             RE_EVEN_BACKSLASHES = r"(?:(?<!\\)(?:\\\\)*)"
392             naked_string = string[len(string_prefix) + 1 : -1]
393             naked_string = re.sub(
394                 "(" + RE_EVEN_BACKSLASHES + ")" + QUOTE, r"\1\\" + QUOTE, naked_string
395             )
396             return naked_string
397
398         # Holds the CustomSplit objects that will later be added to the custom
399         # split map.
400         custom_splits = []
401
402         # Temporary storage for the 'has_prefix' part of the CustomSplit objects.
403         prefix_tracker = []
404
405         # Sets the 'prefix' variable. This is the prefix that the final merged
406         # string will have.
407         next_str_idx = string_idx
408         prefix = ""
409         while (
410             not prefix
411             and is_valid_index(next_str_idx)
412             and LL[next_str_idx].type == token.STRING
413         ):
414             prefix = get_string_prefix(LL[next_str_idx].value)
415             next_str_idx += 1
416
417         # The next loop merges the string group. The final string will be
418         # contained in 'S'.
419         #
420         # The following convenience variables are used:
421         #
422         #   S: string
423         #   NS: naked string
424         #   SS: next string
425         #   NSS: naked next string
426         S = ""
427         NS = ""
428         num_of_strings = 0
429         next_str_idx = string_idx
430         while is_valid_index(next_str_idx) and LL[next_str_idx].type == token.STRING:
431             num_of_strings += 1
432
433             SS = LL[next_str_idx].value
434             next_prefix = get_string_prefix(SS)
435
436             # If this is an f-string group but this substring is not prefixed
437             # with 'f'...
438             if "f" in prefix and "f" not in next_prefix:
439                 # Then we must escape any braces contained in this substring.
440                 SS = re.subf(r"(\{|\})", "{1}{1}", SS)
441
442             NSS = make_naked(SS, next_prefix)
443
444             has_prefix = bool(next_prefix)
445             prefix_tracker.append(has_prefix)
446
447             S = prefix + QUOTE + NS + NSS + BREAK_MARK + QUOTE
448             NS = make_naked(S, prefix)
449
450             next_str_idx += 1
451
452         S_leaf = Leaf(token.STRING, S)
453         if self.normalize_strings:
454             S_leaf.value = normalize_string_quotes(S_leaf.value)
455
456         # Fill the 'custom_splits' list with the appropriate CustomSplit objects.
457         temp_string = S_leaf.value[len(prefix) + 1 : -1]
458         for has_prefix in prefix_tracker:
459             mark_idx = temp_string.find(BREAK_MARK)
460             assert (
461                 mark_idx >= 0
462             ), "Logic error while filling the custom string breakpoint cache."
463
464             temp_string = temp_string[mark_idx + len(BREAK_MARK) :]
465             breakpoint_idx = mark_idx + (len(prefix) if has_prefix else 0) + 1
466             custom_splits.append(CustomSplit(has_prefix, breakpoint_idx))
467
468         string_leaf = Leaf(token.STRING, S_leaf.value.replace(BREAK_MARK, ""))
469
470         if atom_node is not None:
471             replace_child(atom_node, string_leaf)
472
473         # Build the final line ('new_line') that this method will later return.
474         new_line = line.clone()
475         for (i, leaf) in enumerate(LL):
476             if i == string_idx:
477                 new_line.append(string_leaf)
478
479             if string_idx <= i < string_idx + num_of_strings:
480                 for comment_leaf in line.comments_after(LL[i]):
481                     new_line.append(comment_leaf, preformatted=True)
482                 continue
483
484             append_leaves(new_line, line, [leaf])
485
486         self.add_custom_splits(string_leaf.value, custom_splits)
487         return Ok(new_line)
488
489     @staticmethod
490     def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
491         """Validate (M)erge (S)tring (G)roup
492
493         Transform-time string validation logic for __merge_string_group(...).
494
495         Returns:
496             * Ok(None), if ALL validation checks (listed below) pass.
497                 OR
498             * Err(CannotTransform), if any of the following are true:
499                 - The target string group does not contain ANY stand-alone comments.
500                 - The target string is not in a string group (i.e. it has no
501                   adjacent strings).
502                 - The string group has more than one inline comment.
503                 - The string group has an inline comment that appears to be a pragma.
504                 - The set of all string prefixes in the string group is of
505                   length greater than one and is not equal to {"", "f"}.
506                 - The string group consists of raw strings.
507         """
508         # We first check for "inner" stand-alone comments (i.e. stand-alone
509         # comments that have a string leaf before them AND after them).
510         for inc in [1, -1]:
511             i = string_idx
512             found_sa_comment = False
513             is_valid_index = is_valid_index_factory(line.leaves)
514             while is_valid_index(i) and line.leaves[i].type in [
515                 token.STRING,
516                 STANDALONE_COMMENT,
517             ]:
518                 if line.leaves[i].type == STANDALONE_COMMENT:
519                     found_sa_comment = True
520                 elif found_sa_comment:
521                     return TErr(
522                         "StringMerger does NOT merge string groups which contain "
523                         "stand-alone comments."
524                     )
525
526                 i += inc
527
528         num_of_inline_string_comments = 0
529         set_of_prefixes = set()
530         num_of_strings = 0
531         for leaf in line.leaves[string_idx:]:
532             if leaf.type != token.STRING:
533                 # If the string group is trailed by a comma, we count the
534                 # comments trailing the comma to be one of the string group's
535                 # comments.
536                 if leaf.type == token.COMMA and id(leaf) in line.comments:
537                     num_of_inline_string_comments += 1
538                 break
539
540             if has_triple_quotes(leaf.value):
541                 return TErr("StringMerger does NOT merge multiline strings.")
542
543             num_of_strings += 1
544             prefix = get_string_prefix(leaf.value)
545             if "r" in prefix:
546                 return TErr("StringMerger does NOT merge raw strings.")
547
548             set_of_prefixes.add(prefix)
549
550             if id(leaf) in line.comments:
551                 num_of_inline_string_comments += 1
552                 if contains_pragma_comment(line.comments[id(leaf)]):
553                     return TErr("Cannot merge strings which have pragma comments.")
554
555         if num_of_strings < 2:
556             return TErr(
557                 f"Not enough strings to merge (num_of_strings={num_of_strings})."
558             )
559
560         if num_of_inline_string_comments > 1:
561             return TErr(
562                 f"Too many inline string comments ({num_of_inline_string_comments})."
563             )
564
565         if len(set_of_prefixes) > 1 and set_of_prefixes != {"", "f"}:
566             return TErr(f"Too many different prefixes ({set_of_prefixes}).")
567
568         return Ok(None)
569
570
571 class StringParenStripper(StringTransformer):
572     """StringTransformer that strips surrounding parentheses from strings.
573
574     Requirements:
575         The line contains a string which is surrounded by parentheses and:
576             - The target string is NOT the only argument to a function call.
577             - The target string is NOT a "pointless" string.
578             - If the target string contains a PERCENT, the brackets are not
579               preceded or followed by an operator with higher precedence than
580               PERCENT.
581
582     Transformations:
583         The parentheses mentioned in the 'Requirements' section are stripped.
584
585     Collaborations:
586         StringParenStripper has its own inherent usefulness, but it is also
587         relied on to clean up the parentheses created by StringParenWrapper (in
588         the event that they are no longer needed).
589     """
590
591     def do_match(self, line: Line) -> TMatchResult:
592         LL = line.leaves
593
594         is_valid_index = is_valid_index_factory(LL)
595
596         for (idx, leaf) in enumerate(LL):
597             # Should be a string...
598             if leaf.type != token.STRING:
599                 continue
600
601             # If this is a "pointless" string...
602             if (
603                 leaf.parent
604                 and leaf.parent.parent
605                 and leaf.parent.parent.type == syms.simple_stmt
606             ):
607                 continue
608
609             # Should be preceded by a non-empty LPAR...
610             if (
611                 not is_valid_index(idx - 1)
612                 or LL[idx - 1].type != token.LPAR
613                 or is_empty_lpar(LL[idx - 1])
614             ):
615                 continue
616
617             # That LPAR should NOT be preceded by a function name or a closing
618             # bracket (which could be a function which returns a function or a
619             # list/dictionary that contains a function)...
620             if is_valid_index(idx - 2) and (
621                 LL[idx - 2].type == token.NAME or LL[idx - 2].type in CLOSING_BRACKETS
622             ):
623                 continue
624
625             string_idx = idx
626
627             # Skip the string trailer, if one exists.
628             string_parser = StringParser()
629             next_idx = string_parser.parse(LL, string_idx)
630
631             # if the leaves in the parsed string include a PERCENT, we need to
632             # make sure the initial LPAR is NOT preceded by an operator with
633             # higher or equal precedence to PERCENT
634             if is_valid_index(idx - 2):
635                 # mypy can't quite follow unless we name this
636                 before_lpar = LL[idx - 2]
637                 if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and (
638                     (
639                         before_lpar.type
640                         in {
641                             token.STAR,
642                             token.AT,
643                             token.SLASH,
644                             token.DOUBLESLASH,
645                             token.PERCENT,
646                             token.TILDE,
647                             token.DOUBLESTAR,
648                             token.AWAIT,
649                             token.LSQB,
650                             token.LPAR,
651                         }
652                     )
653                     or (
654                         # only unary PLUS/MINUS
655                         before_lpar.parent
656                         and before_lpar.parent.type == syms.factor
657                         and (before_lpar.type in {token.PLUS, token.MINUS})
658                     )
659                 ):
660                     continue
661
662             # Should be followed by a non-empty RPAR...
663             if (
664                 is_valid_index(next_idx)
665                 and LL[next_idx].type == token.RPAR
666                 and not is_empty_rpar(LL[next_idx])
667             ):
668                 # That RPAR should NOT be followed by anything with higher
669                 # precedence than PERCENT
670                 if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in {
671                     token.DOUBLESTAR,
672                     token.LSQB,
673                     token.LPAR,
674                     token.DOT,
675                 }:
676                     continue
677
678                 return Ok(string_idx)
679
680         return TErr("This line has no strings wrapped in parens.")
681
682     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
683         LL = line.leaves
684
685         string_parser = StringParser()
686         rpar_idx = string_parser.parse(LL, string_idx)
687
688         for leaf in (LL[string_idx - 1], LL[rpar_idx]):
689             if line.comments_after(leaf):
690                 yield TErr(
691                     "Will not strip parentheses which have comments attached to them."
692                 )
693                 return
694
695         new_line = line.clone()
696         new_line.comments = line.comments.copy()
697         try:
698             append_leaves(new_line, line, LL[: string_idx - 1])
699         except BracketMatchError:
700             # HACK: I believe there is currently a bug somewhere in
701             # right_hand_split() that is causing brackets to not be tracked
702             # properly by a shared BracketTracker.
703             append_leaves(new_line, line, LL[: string_idx - 1], preformatted=True)
704
705         string_leaf = Leaf(token.STRING, LL[string_idx].value)
706         LL[string_idx - 1].remove()
707         replace_child(LL[string_idx], string_leaf)
708         new_line.append(string_leaf)
709
710         append_leaves(
711             new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :]
712         )
713
714         LL[rpar_idx].remove()
715
716         yield Ok(new_line)
717
718
719 class BaseStringSplitter(StringTransformer):
720     """
721     Abstract class for StringTransformers which transform a Line's strings by splitting
722     them or placing them on their own lines where necessary to avoid going over
723     the configured line length.
724
725     Requirements:
726         * The target string value is responsible for the line going over the
727         line length limit. It follows that after all of black's other line
728         split methods have been exhausted, this line (or one of the resulting
729         lines after all line splits are performed) would still be over the
730         line_length limit unless we split this string.
731             AND
732         * The target string is NOT a "pointless" string (i.e. a string that has
733         no parent or siblings).
734             AND
735         * The target string is not followed by an inline comment that appears
736         to be a pragma.
737             AND
738         * The target string is not a multiline (i.e. triple-quote) string.
739     """
740
741     @abstractmethod
742     def do_splitter_match(self, line: Line) -> TMatchResult:
743         """
744         BaseStringSplitter asks its clients to override this method instead of
745         `StringTransformer.do_match(...)`.
746
747         Follows the same protocol as `StringTransformer.do_match(...)`.
748
749         Refer to `help(StringTransformer.do_match)` for more information.
750         """
751
752     def do_match(self, line: Line) -> TMatchResult:
753         match_result = self.do_splitter_match(line)
754         if isinstance(match_result, Err):
755             return match_result
756
757         string_idx = match_result.ok()
758         vresult = self._validate(line, string_idx)
759         if isinstance(vresult, Err):
760             return vresult
761
762         return match_result
763
764     def _validate(self, line: Line, string_idx: int) -> TResult[None]:
765         """
766         Checks that @line meets all of the requirements listed in this classes'
767         docstring. Refer to `help(BaseStringSplitter)` for a detailed
768         description of those requirements.
769
770         Returns:
771             * Ok(None), if ALL of the requirements are met.
772                 OR
773             * Err(CannotTransform), if ANY of the requirements are NOT met.
774         """
775         LL = line.leaves
776
777         string_leaf = LL[string_idx]
778
779         max_string_length = self._get_max_string_length(line, string_idx)
780         if len(string_leaf.value) <= max_string_length:
781             return TErr(
782                 "The string itself is not what is causing this line to be too long."
783             )
784
785         if not string_leaf.parent or [L.type for L in string_leaf.parent.children] == [
786             token.STRING,
787             token.NEWLINE,
788         ]:
789             return TErr(
790                 f"This string ({string_leaf.value}) appears to be pointless (i.e. has"
791                 " no parent)."
792             )
793
794         if id(line.leaves[string_idx]) in line.comments and contains_pragma_comment(
795             line.comments[id(line.leaves[string_idx])]
796         ):
797             return TErr(
798                 "Line appears to end with an inline pragma comment. Splitting the line"
799                 " could modify the pragma's behavior."
800             )
801
802         if has_triple_quotes(string_leaf.value):
803             return TErr("We cannot split multiline strings.")
804
805         return Ok(None)
806
807     def _get_max_string_length(self, line: Line, string_idx: int) -> int:
808         """
809         Calculates the max string length used when attempting to determine
810         whether or not the target string is responsible for causing the line to
811         go over the line length limit.
812
813         WARNING: This method is tightly coupled to both StringSplitter and
814         (especially) StringParenWrapper. There is probably a better way to
815         accomplish what is being done here.
816
817         Returns:
818             max_string_length: such that `line.leaves[string_idx].value >
819             max_string_length` implies that the target string IS responsible
820             for causing this line to exceed the line length limit.
821         """
822         LL = line.leaves
823
824         is_valid_index = is_valid_index_factory(LL)
825
826         # We use the shorthand "WMA4" in comments to abbreviate "We must
827         # account for". When giving examples, we use STRING to mean some/any
828         # valid string.
829         #
830         # Finally, we use the following convenience variables:
831         #
832         #   P:  The leaf that is before the target string leaf.
833         #   N:  The leaf that is after the target string leaf.
834         #   NN: The leaf that is after N.
835
836         # WMA4 the whitespace at the beginning of the line.
837         offset = line.depth * 4
838
839         if is_valid_index(string_idx - 1):
840             p_idx = string_idx - 1
841             if (
842                 LL[string_idx - 1].type == token.LPAR
843                 and LL[string_idx - 1].value == ""
844                 and string_idx >= 2
845             ):
846                 # If the previous leaf is an empty LPAR placeholder, we should skip it.
847                 p_idx -= 1
848
849             P = LL[p_idx]
850             if P.type == token.PLUS:
851                 # WMA4 a space and a '+' character (e.g. `+ STRING`).
852                 offset += 2
853
854             if P.type == token.COMMA:
855                 # WMA4 a space, a comma, and a closing bracket [e.g. `), STRING`].
856                 offset += 3
857
858             if P.type in [token.COLON, token.EQUAL, token.NAME]:
859                 # This conditional branch is meant to handle dictionary keys,
860                 # variable assignments, 'return STRING' statement lines, and
861                 # 'else STRING' ternary expression lines.
862
863                 # WMA4 a single space.
864                 offset += 1
865
866                 # WMA4 the lengths of any leaves that came before that space,
867                 # but after any closing bracket before that space.
868                 for leaf in reversed(LL[: p_idx + 1]):
869                     offset += len(str(leaf))
870                     if leaf.type in CLOSING_BRACKETS:
871                         break
872
873         if is_valid_index(string_idx + 1):
874             N = LL[string_idx + 1]
875             if N.type == token.RPAR and N.value == "" and len(LL) > string_idx + 2:
876                 # If the next leaf is an empty RPAR placeholder, we should skip it.
877                 N = LL[string_idx + 2]
878
879             if N.type == token.COMMA:
880                 # WMA4 a single comma at the end of the string (e.g `STRING,`).
881                 offset += 1
882
883             if is_valid_index(string_idx + 2):
884                 NN = LL[string_idx + 2]
885
886                 if N.type == token.DOT and NN.type == token.NAME:
887                     # This conditional branch is meant to handle method calls invoked
888                     # off of a string literal up to and including the LPAR character.
889
890                     # WMA4 the '.' character.
891                     offset += 1
892
893                     if (
894                         is_valid_index(string_idx + 3)
895                         and LL[string_idx + 3].type == token.LPAR
896                     ):
897                         # WMA4 the left parenthesis character.
898                         offset += 1
899
900                     # WMA4 the length of the method's name.
901                     offset += len(NN.value)
902
903         has_comments = False
904         for comment_leaf in line.comments_after(LL[string_idx]):
905             if not has_comments:
906                 has_comments = True
907                 # WMA4 two spaces before the '#' character.
908                 offset += 2
909
910             # WMA4 the length of the inline comment.
911             offset += len(comment_leaf.value)
912
913         max_string_length = self.line_length - offset
914         return max_string_length
915
916
917 class StringSplitter(CustomSplitMapMixin, BaseStringSplitter):
918     """
919     StringTransformer that splits "atom" strings (i.e. strings which exist on
920     lines by themselves).
921
922     Requirements:
923         * The line consists ONLY of a single string (with the exception of a
924         '+' symbol which MAY exist at the start of the line), MAYBE a string
925         trailer, and MAYBE a trailing comma.
926             AND
927         * All of the requirements listed in BaseStringSplitter's docstring.
928
929     Transformations:
930         The string mentioned in the 'Requirements' section is split into as
931         many substrings as necessary to adhere to the configured line length.
932
933         In the final set of substrings, no substring should be smaller than
934         MIN_SUBSTR_SIZE characters.
935
936         The string will ONLY be split on spaces (i.e. each new substring should
937         start with a space). Note that the string will NOT be split on a space
938         which is escaped with a backslash.
939
940         If the string is an f-string, it will NOT be split in the middle of an
941         f-expression (e.g. in f"FooBar: {foo() if x else bar()}", {foo() if x
942         else bar()} is an f-expression).
943
944         If the string that is being split has an associated set of custom split
945         records and those custom splits will NOT result in any line going over
946         the configured line length, those custom splits are used. Otherwise the
947         string is split as late as possible (from left-to-right) while still
948         adhering to the transformation rules listed above.
949
950     Collaborations:
951         StringSplitter relies on StringMerger to construct the appropriate
952         CustomSplit objects and add them to the custom split map.
953     """
954
955     MIN_SUBSTR_SIZE = 6
956     # Matches an "f-expression" (e.g. {var}) that might be found in an f-string.
957     RE_FEXPR = r"""
958     (?<!\{) (?:\{\{)* \{ (?!\{)
959         (?:
960             [^\{\}]
961             | \{\{
962             | \}\}
963             | (?R)
964         )+?
965     (?<!\}) \} (?:\}\})* (?!\})
966     """
967
968     def do_splitter_match(self, line: Line) -> TMatchResult:
969         LL = line.leaves
970
971         is_valid_index = is_valid_index_factory(LL)
972
973         idx = 0
974
975         # The first leaf MAY be a '+' symbol...
976         if is_valid_index(idx) and LL[idx].type == token.PLUS:
977             idx += 1
978
979         # The next/first leaf MAY be an empty LPAR...
980         if is_valid_index(idx) and is_empty_lpar(LL[idx]):
981             idx += 1
982
983         # The next/first leaf MUST be a string...
984         if not is_valid_index(idx) or LL[idx].type != token.STRING:
985             return TErr("Line does not start with a string.")
986
987         string_idx = idx
988
989         # Skip the string trailer, if one exists.
990         string_parser = StringParser()
991         idx = string_parser.parse(LL, string_idx)
992
993         # That string MAY be followed by an empty RPAR...
994         if is_valid_index(idx) and is_empty_rpar(LL[idx]):
995             idx += 1
996
997         # That string / empty RPAR leaf MAY be followed by a comma...
998         if is_valid_index(idx) and LL[idx].type == token.COMMA:
999             idx += 1
1000
1001         # But no more leaves are allowed...
1002         if is_valid_index(idx):
1003             return TErr("This line does not end with a string.")
1004
1005         return Ok(string_idx)
1006
1007     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1008         LL = line.leaves
1009
1010         QUOTE = LL[string_idx].value[-1]
1011
1012         is_valid_index = is_valid_index_factory(LL)
1013         insert_str_child = insert_str_child_factory(LL[string_idx])
1014
1015         prefix = get_string_prefix(LL[string_idx].value)
1016
1017         # We MAY choose to drop the 'f' prefix from substrings that don't
1018         # contain any f-expressions, but ONLY if the original f-string
1019         # contains at least one f-expression. Otherwise, we will alter the AST
1020         # of the program.
1021         drop_pointless_f_prefix = ("f" in prefix) and re.search(
1022             self.RE_FEXPR, LL[string_idx].value, re.VERBOSE
1023         )
1024
1025         first_string_line = True
1026         starts_with_plus = LL[0].type == token.PLUS
1027
1028         def line_needs_plus() -> bool:
1029             return first_string_line and starts_with_plus
1030
1031         def maybe_append_plus(new_line: Line) -> None:
1032             """
1033             Side Effects:
1034                 If @line starts with a plus and this is the first line we are
1035                 constructing, this function appends a PLUS leaf to @new_line
1036                 and replaces the old PLUS leaf in the node structure. Otherwise
1037                 this function does nothing.
1038             """
1039             if line_needs_plus():
1040                 plus_leaf = Leaf(token.PLUS, "+")
1041                 replace_child(LL[0], plus_leaf)
1042                 new_line.append(plus_leaf)
1043
1044         ends_with_comma = (
1045             is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1046         )
1047
1048         def max_last_string() -> int:
1049             """
1050             Returns:
1051                 The max allowed length of the string value used for the last
1052                 line we will construct.
1053             """
1054             result = self.line_length
1055             result -= line.depth * 4
1056             result -= 1 if ends_with_comma else 0
1057             result -= 2 if line_needs_plus() else 0
1058             return result
1059
1060         # --- Calculate Max Break Index (for string value)
1061         # We start with the line length limit
1062         max_break_idx = self.line_length
1063         # The last index of a string of length N is N-1.
1064         max_break_idx -= 1
1065         # Leading whitespace is not present in the string value (e.g. Leaf.value).
1066         max_break_idx -= line.depth * 4
1067         if max_break_idx < 0:
1068             yield TErr(
1069                 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1070                 f" {line.depth}"
1071             )
1072             return
1073
1074         # Check if StringMerger registered any custom splits.
1075         custom_splits = self.pop_custom_splits(LL[string_idx].value)
1076         # We use them ONLY if none of them would produce lines that exceed the
1077         # line limit.
1078         use_custom_breakpoints = bool(
1079             custom_splits
1080             and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
1081         )
1082
1083         # Temporary storage for the remaining chunk of the string line that
1084         # can't fit onto the line currently being constructed.
1085         rest_value = LL[string_idx].value
1086
1087         def more_splits_should_be_made() -> bool:
1088             """
1089             Returns:
1090                 True iff `rest_value` (the remaining string value from the last
1091                 split), should be split again.
1092             """
1093             if use_custom_breakpoints:
1094                 return len(custom_splits) > 1
1095             else:
1096                 return len(rest_value) > max_last_string()
1097
1098         string_line_results: List[Ok[Line]] = []
1099         while more_splits_should_be_made():
1100             if use_custom_breakpoints:
1101                 # Custom User Split (manual)
1102                 csplit = custom_splits.pop(0)
1103                 break_idx = csplit.break_idx
1104             else:
1105                 # Algorithmic Split (automatic)
1106                 max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx
1107                 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
1108                 if maybe_break_idx is None:
1109                     # If we are unable to algorithmically determine a good split
1110                     # and this string has custom splits registered to it, we
1111                     # fall back to using them--which means we have to start
1112                     # over from the beginning.
1113                     if custom_splits:
1114                         rest_value = LL[string_idx].value
1115                         string_line_results = []
1116                         first_string_line = True
1117                         use_custom_breakpoints = True
1118                         continue
1119
1120                     # Otherwise, we stop splitting here.
1121                     break
1122
1123                 break_idx = maybe_break_idx
1124
1125             # --- Construct `next_value`
1126             next_value = rest_value[:break_idx] + QUOTE
1127             if (
1128                 # Are we allowed to try to drop a pointless 'f' prefix?
1129                 drop_pointless_f_prefix
1130                 # If we are, will we be successful?
1131                 and next_value != self._normalize_f_string(next_value, prefix)
1132             ):
1133                 # If the current custom split did NOT originally use a prefix,
1134                 # then `csplit.break_idx` will be off by one after removing
1135                 # the 'f' prefix.
1136                 break_idx = (
1137                     break_idx + 1
1138                     if use_custom_breakpoints and not csplit.has_prefix
1139                     else break_idx
1140                 )
1141                 next_value = rest_value[:break_idx] + QUOTE
1142                 next_value = self._normalize_f_string(next_value, prefix)
1143
1144             # --- Construct `next_leaf`
1145             next_leaf = Leaf(token.STRING, next_value)
1146             insert_str_child(next_leaf)
1147             self._maybe_normalize_string_quotes(next_leaf)
1148
1149             # --- Construct `next_line`
1150             next_line = line.clone()
1151             maybe_append_plus(next_line)
1152             next_line.append(next_leaf)
1153             string_line_results.append(Ok(next_line))
1154
1155             rest_value = prefix + QUOTE + rest_value[break_idx:]
1156             first_string_line = False
1157
1158         yield from string_line_results
1159
1160         if drop_pointless_f_prefix:
1161             rest_value = self._normalize_f_string(rest_value, prefix)
1162
1163         rest_leaf = Leaf(token.STRING, rest_value)
1164         insert_str_child(rest_leaf)
1165
1166         # NOTE: I could not find a test case that verifies that the following
1167         # line is actually necessary, but it seems to be. Otherwise we risk
1168         # not normalizing the last substring, right?
1169         self._maybe_normalize_string_quotes(rest_leaf)
1170
1171         last_line = line.clone()
1172         maybe_append_plus(last_line)
1173
1174         # If there are any leaves to the right of the target string...
1175         if is_valid_index(string_idx + 1):
1176             # We use `temp_value` here to determine how long the last line
1177             # would be if we were to append all the leaves to the right of the
1178             # target string to the last string line.
1179             temp_value = rest_value
1180             for leaf in LL[string_idx + 1 :]:
1181                 temp_value += str(leaf)
1182                 if leaf.type == token.LPAR:
1183                     break
1184
1185             # Try to fit them all on the same line with the last substring...
1186             if (
1187                 len(temp_value) <= max_last_string()
1188                 or LL[string_idx + 1].type == token.COMMA
1189             ):
1190                 last_line.append(rest_leaf)
1191                 append_leaves(last_line, line, LL[string_idx + 1 :])
1192                 yield Ok(last_line)
1193             # Otherwise, place the last substring on one line and everything
1194             # else on a line below that...
1195             else:
1196                 last_line.append(rest_leaf)
1197                 yield Ok(last_line)
1198
1199                 non_string_line = line.clone()
1200                 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1201                 yield Ok(non_string_line)
1202         # Else the target string was the last leaf...
1203         else:
1204             last_line.append(rest_leaf)
1205             last_line.comments = line.comments.copy()
1206             yield Ok(last_line)
1207
1208     def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
1209         """
1210         This method contains the algorithm that StringSplitter uses to
1211         determine which character to split each string at.
1212
1213         Args:
1214             @string: The substring that we are attempting to split.
1215             @max_break_idx: The ideal break index. We will return this value if it
1216             meets all the necessary conditions. In the likely event that it
1217             doesn't we will try to find the closest index BELOW @max_break_idx
1218             that does. If that fails, we will expand our search by also
1219             considering all valid indices ABOVE @max_break_idx.
1220
1221         Pre-Conditions:
1222             * assert_is_leaf_string(@string)
1223             * 0 <= @max_break_idx < len(@string)
1224
1225         Returns:
1226             break_idx, if an index is able to be found that meets all of the
1227             conditions listed in the 'Transformations' section of this classes'
1228             docstring.
1229                 OR
1230             None, otherwise.
1231         """
1232         is_valid_index = is_valid_index_factory(string)
1233
1234         assert is_valid_index(max_break_idx)
1235         assert_is_leaf_string(string)
1236
1237         _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None
1238
1239         def fexpr_slices() -> Iterator[Tuple[Index, Index]]:
1240             """
1241             Yields:
1242                 All ranges of @string which, if @string were to be split there,
1243                 would result in the splitting of an f-expression (which is NOT
1244                 allowed).
1245             """
1246             nonlocal _fexpr_slices
1247
1248             if _fexpr_slices is None:
1249                 _fexpr_slices = []
1250                 for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE):
1251                     _fexpr_slices.append(match.span())
1252
1253             yield from _fexpr_slices
1254
1255         is_fstring = "f" in get_string_prefix(string)
1256
1257         def breaks_fstring_expression(i: Index) -> bool:
1258             """
1259             Returns:
1260                 True iff returning @i would result in the splitting of an
1261                 f-expression (which is NOT allowed).
1262             """
1263             if not is_fstring:
1264                 return False
1265
1266             for (start, end) in fexpr_slices():
1267                 if start <= i < end:
1268                     return True
1269
1270             return False
1271
1272         def passes_all_checks(i: Index) -> bool:
1273             """
1274             Returns:
1275                 True iff ALL of the conditions listed in the 'Transformations'
1276                 section of this classes' docstring would be be met by returning @i.
1277             """
1278             is_space = string[i] == " "
1279
1280             is_not_escaped = True
1281             j = i - 1
1282             while is_valid_index(j) and string[j] == "\\":
1283                 is_not_escaped = not is_not_escaped
1284                 j -= 1
1285
1286             is_big_enough = (
1287                 len(string[i:]) >= self.MIN_SUBSTR_SIZE
1288                 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
1289             )
1290             return (
1291                 is_space
1292                 and is_not_escaped
1293                 and is_big_enough
1294                 and not breaks_fstring_expression(i)
1295             )
1296
1297         # First, we check all indices BELOW @max_break_idx.
1298         break_idx = max_break_idx
1299         while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1300             break_idx -= 1
1301
1302         if not passes_all_checks(break_idx):
1303             # If that fails, we check all indices ABOVE @max_break_idx.
1304             #
1305             # If we are able to find a valid index here, the next line is going
1306             # to be longer than the specified line length, but it's probably
1307             # better than doing nothing at all.
1308             break_idx = max_break_idx + 1
1309             while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1310                 break_idx += 1
1311
1312             if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1313                 return None
1314
1315         return break_idx
1316
1317     def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1318         if self.normalize_strings:
1319             leaf.value = normalize_string_quotes(leaf.value)
1320
1321     def _normalize_f_string(self, string: str, prefix: str) -> str:
1322         """
1323         Pre-Conditions:
1324             * assert_is_leaf_string(@string)
1325
1326         Returns:
1327             * If @string is an f-string that contains no f-expressions, we
1328             return a string identical to @string except that the 'f' prefix
1329             has been stripped and all double braces (i.e. '{{' or '}}') have
1330             been normalized (i.e. turned into '{' or '}').
1331                 OR
1332             * Otherwise, we return @string.
1333         """
1334         assert_is_leaf_string(string)
1335
1336         if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE):
1337             new_prefix = prefix.replace("f", "")
1338
1339             temp = string[len(prefix) :]
1340             temp = re.sub(r"\{\{", "{", temp)
1341             temp = re.sub(r"\}\}", "}", temp)
1342             new_string = temp
1343
1344             return f"{new_prefix}{new_string}"
1345         else:
1346             return string
1347
1348
1349 class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
1350     """
1351     StringTransformer that splits non-"atom" strings (i.e. strings that do not
1352     exist on lines by themselves).
1353
1354     Requirements:
1355         All of the requirements listed in BaseStringSplitter's docstring in
1356         addition to the requirements listed below:
1357
1358         * The line is a return/yield statement, which returns/yields a string.
1359             OR
1360         * The line is part of a ternary expression (e.g. `x = y if cond else
1361         z`) such that the line starts with `else <string>`, where <string> is
1362         some string.
1363             OR
1364         * The line is an assert statement, which ends with a string.
1365             OR
1366         * The line is an assignment statement (e.g. `x = <string>` or `x +=
1367         <string>`) such that the variable is being assigned the value of some
1368         string.
1369             OR
1370         * The line is a dictionary key assignment where some valid key is being
1371         assigned the value of some string.
1372
1373     Transformations:
1374         The chosen string is wrapped in parentheses and then split at the LPAR.
1375
1376         We then have one line which ends with an LPAR and another line that
1377         starts with the chosen string. The latter line is then split again at
1378         the RPAR. This results in the RPAR (and possibly a trailing comma)
1379         being placed on its own line.
1380
1381         NOTE: If any leaves exist to the right of the chosen string (except
1382         for a trailing comma, which would be placed after the RPAR), those
1383         leaves are placed inside the parentheses.  In effect, the chosen
1384         string is not necessarily being "wrapped" by parentheses. We can,
1385         however, count on the LPAR being placed directly before the chosen
1386         string.
1387
1388         In other words, StringParenWrapper creates "atom" strings. These
1389         can then be split again by StringSplitter, if necessary.
1390
1391     Collaborations:
1392         In the event that a string line split by StringParenWrapper is
1393         changed such that it no longer needs to be given its own line,
1394         StringParenWrapper relies on StringParenStripper to clean up the
1395         parentheses it created.
1396     """
1397
1398     def do_splitter_match(self, line: Line) -> TMatchResult:
1399         LL = line.leaves
1400
1401         if line.leaves[-1].type in OPENING_BRACKETS:
1402             return TErr(
1403                 "Cannot wrap parens around a line that ends in an opening bracket."
1404             )
1405
1406         string_idx = (
1407             self._return_match(LL)
1408             or self._else_match(LL)
1409             or self._assert_match(LL)
1410             or self._assign_match(LL)
1411             or self._dict_match(LL)
1412         )
1413
1414         if string_idx is not None:
1415             string_value = line.leaves[string_idx].value
1416             # If the string has no spaces...
1417             if " " not in string_value:
1418                 # And will still violate the line length limit when split...
1419                 max_string_length = self.line_length - ((line.depth + 1) * 4)
1420                 if len(string_value) > max_string_length:
1421                     # And has no associated custom splits...
1422                     if not self.has_custom_splits(string_value):
1423                         # Then we should NOT put this string on its own line.
1424                         return TErr(
1425                             "We do not wrap long strings in parentheses when the"
1426                             " resultant line would still be over the specified line"
1427                             " length and can't be split further by StringSplitter."
1428                         )
1429             return Ok(string_idx)
1430
1431         return TErr("This line does not contain any non-atomic strings.")
1432
1433     @staticmethod
1434     def _return_match(LL: List[Leaf]) -> Optional[int]:
1435         """
1436         Returns:
1437             string_idx such that @LL[string_idx] is equal to our target (i.e.
1438             matched) string, if this line matches the return/yield statement
1439             requirements listed in the 'Requirements' section of this classes'
1440             docstring.
1441                 OR
1442             None, otherwise.
1443         """
1444         # If this line is apart of a return/yield statement and the first leaf
1445         # contains either the "return" or "yield" keywords...
1446         if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
1447             0
1448         ].value in ["return", "yield"]:
1449             is_valid_index = is_valid_index_factory(LL)
1450
1451             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1452             # The next visible leaf MUST contain a string...
1453             if is_valid_index(idx) and LL[idx].type == token.STRING:
1454                 return idx
1455
1456         return None
1457
1458     @staticmethod
1459     def _else_match(LL: List[Leaf]) -> Optional[int]:
1460         """
1461         Returns:
1462             string_idx such that @LL[string_idx] is equal to our target (i.e.
1463             matched) string, if this line matches the ternary expression
1464             requirements listed in the 'Requirements' section of this classes'
1465             docstring.
1466                 OR
1467             None, otherwise.
1468         """
1469         # If this line is apart of a ternary expression and the first leaf
1470         # contains the "else" keyword...
1471         if (
1472             parent_type(LL[0]) == syms.test
1473             and LL[0].type == token.NAME
1474             and LL[0].value == "else"
1475         ):
1476             is_valid_index = is_valid_index_factory(LL)
1477
1478             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1479             # The next visible leaf MUST contain a string...
1480             if is_valid_index(idx) and LL[idx].type == token.STRING:
1481                 return idx
1482
1483         return None
1484
1485     @staticmethod
1486     def _assert_match(LL: List[Leaf]) -> Optional[int]:
1487         """
1488         Returns:
1489             string_idx such that @LL[string_idx] is equal to our target (i.e.
1490             matched) string, if this line matches the assert statement
1491             requirements listed in the 'Requirements' section of this classes'
1492             docstring.
1493                 OR
1494             None, otherwise.
1495         """
1496         # If this line is apart of an assert statement and the first leaf
1497         # contains the "assert" keyword...
1498         if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
1499             is_valid_index = is_valid_index_factory(LL)
1500
1501             for (i, leaf) in enumerate(LL):
1502                 # We MUST find a comma...
1503                 if leaf.type == token.COMMA:
1504                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1505
1506                     # That comma MUST be followed by a string...
1507                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1508                         string_idx = idx
1509
1510                         # Skip the string trailer, if one exists.
1511                         string_parser = StringParser()
1512                         idx = string_parser.parse(LL, string_idx)
1513
1514                         # But no more leaves are allowed...
1515                         if not is_valid_index(idx):
1516                             return string_idx
1517
1518         return None
1519
1520     @staticmethod
1521     def _assign_match(LL: List[Leaf]) -> Optional[int]:
1522         """
1523         Returns:
1524             string_idx such that @LL[string_idx] is equal to our target (i.e.
1525             matched) string, if this line matches the assignment statement
1526             requirements listed in the 'Requirements' section of this classes'
1527             docstring.
1528                 OR
1529             None, otherwise.
1530         """
1531         # If this line is apart of an expression statement or is a function
1532         # argument AND the first leaf contains a variable name...
1533         if (
1534             parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
1535             and LL[0].type == token.NAME
1536         ):
1537             is_valid_index = is_valid_index_factory(LL)
1538
1539             for (i, leaf) in enumerate(LL):
1540                 # We MUST find either an '=' or '+=' symbol...
1541                 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
1542                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1543
1544                     # That symbol MUST be followed by a string...
1545                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1546                         string_idx = idx
1547
1548                         # Skip the string trailer, if one exists.
1549                         string_parser = StringParser()
1550                         idx = string_parser.parse(LL, string_idx)
1551
1552                         # The next leaf MAY be a comma iff this line is apart
1553                         # of a function argument...
1554                         if (
1555                             parent_type(LL[0]) == syms.argument
1556                             and is_valid_index(idx)
1557                             and LL[idx].type == token.COMMA
1558                         ):
1559                             idx += 1
1560
1561                         # But no more leaves are allowed...
1562                         if not is_valid_index(idx):
1563                             return string_idx
1564
1565         return None
1566
1567     @staticmethod
1568     def _dict_match(LL: List[Leaf]) -> Optional[int]:
1569         """
1570         Returns:
1571             string_idx such that @LL[string_idx] is equal to our target (i.e.
1572             matched) string, if this line matches the dictionary key assignment
1573             statement requirements listed in the 'Requirements' section of this
1574             classes' docstring.
1575                 OR
1576             None, otherwise.
1577         """
1578         # If this line is apart of a dictionary key assignment...
1579         if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
1580             is_valid_index = is_valid_index_factory(LL)
1581
1582             for (i, leaf) in enumerate(LL):
1583                 # We MUST find a colon...
1584                 if leaf.type == token.COLON:
1585                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1586
1587                     # That colon MUST be followed by a string...
1588                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1589                         string_idx = idx
1590
1591                         # Skip the string trailer, if one exists.
1592                         string_parser = StringParser()
1593                         idx = string_parser.parse(LL, string_idx)
1594
1595                         # That string MAY be followed by a comma...
1596                         if is_valid_index(idx) and LL[idx].type == token.COMMA:
1597                             idx += 1
1598
1599                         # But no more leaves are allowed...
1600                         if not is_valid_index(idx):
1601                             return string_idx
1602
1603         return None
1604
1605     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1606         LL = line.leaves
1607
1608         is_valid_index = is_valid_index_factory(LL)
1609         insert_str_child = insert_str_child_factory(LL[string_idx])
1610
1611         comma_idx = -1
1612         ends_with_comma = False
1613         if LL[comma_idx].type == token.COMMA:
1614             ends_with_comma = True
1615
1616         leaves_to_steal_comments_from = [LL[string_idx]]
1617         if ends_with_comma:
1618             leaves_to_steal_comments_from.append(LL[comma_idx])
1619
1620         # --- First Line
1621         first_line = line.clone()
1622         left_leaves = LL[:string_idx]
1623
1624         # We have to remember to account for (possibly invisible) LPAR and RPAR
1625         # leaves that already wrapped the target string. If these leaves do
1626         # exist, we will replace them with our own LPAR and RPAR leaves.
1627         old_parens_exist = False
1628         if left_leaves and left_leaves[-1].type == token.LPAR:
1629             old_parens_exist = True
1630             leaves_to_steal_comments_from.append(left_leaves[-1])
1631             left_leaves.pop()
1632
1633         append_leaves(first_line, line, left_leaves)
1634
1635         lpar_leaf = Leaf(token.LPAR, "(")
1636         if old_parens_exist:
1637             replace_child(LL[string_idx - 1], lpar_leaf)
1638         else:
1639             insert_str_child(lpar_leaf)
1640         first_line.append(lpar_leaf)
1641
1642         # We throw inline comments that were originally to the right of the
1643         # target string to the top line. They will now be shown to the right of
1644         # the LPAR.
1645         for leaf in leaves_to_steal_comments_from:
1646             for comment_leaf in line.comments_after(leaf):
1647                 first_line.append(comment_leaf, preformatted=True)
1648
1649         yield Ok(first_line)
1650
1651         # --- Middle (String) Line
1652         # We only need to yield one (possibly too long) string line, since the
1653         # `StringSplitter` will break it down further if necessary.
1654         string_value = LL[string_idx].value
1655         string_line = Line(
1656             mode=line.mode,
1657             depth=line.depth + 1,
1658             inside_brackets=True,
1659             should_split_rhs=line.should_split_rhs,
1660             magic_trailing_comma=line.magic_trailing_comma,
1661         )
1662         string_leaf = Leaf(token.STRING, string_value)
1663         insert_str_child(string_leaf)
1664         string_line.append(string_leaf)
1665
1666         old_rpar_leaf = None
1667         if is_valid_index(string_idx + 1):
1668             right_leaves = LL[string_idx + 1 :]
1669             if ends_with_comma:
1670                 right_leaves.pop()
1671
1672             if old_parens_exist:
1673                 assert right_leaves and right_leaves[-1].type == token.RPAR, (
1674                     "Apparently, old parentheses do NOT exist?!"
1675                     f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
1676                 )
1677                 old_rpar_leaf = right_leaves.pop()
1678
1679             append_leaves(string_line, line, right_leaves)
1680
1681         yield Ok(string_line)
1682
1683         # --- Last Line
1684         last_line = line.clone()
1685         last_line.bracket_tracker = first_line.bracket_tracker
1686
1687         new_rpar_leaf = Leaf(token.RPAR, ")")
1688         if old_rpar_leaf is not None:
1689             replace_child(old_rpar_leaf, new_rpar_leaf)
1690         else:
1691             insert_str_child(new_rpar_leaf)
1692         last_line.append(new_rpar_leaf)
1693
1694         # If the target string ended with a comma, we place this comma to the
1695         # right of the RPAR on the last line.
1696         if ends_with_comma:
1697             comma_leaf = Leaf(token.COMMA, ",")
1698             replace_child(LL[comma_idx], comma_leaf)
1699             last_line.append(comma_leaf)
1700
1701         yield Ok(last_line)
1702
1703
1704 class StringParser:
1705     """
1706     A state machine that aids in parsing a string's "trailer", which can be
1707     either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
1708     (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
1709     varY)`).
1710
1711     NOTE: A new StringParser object MUST be instantiated for each string
1712     trailer we need to parse.
1713
1714     Examples:
1715         We shall assume that `line` equals the `Line` object that corresponds
1716         to the following line of python code:
1717         ```
1718         x = "Some {}.".format("String") + some_other_string
1719         ```
1720
1721         Furthermore, we will assume that `string_idx` is some index such that:
1722         ```
1723         assert line.leaves[string_idx].value == "Some {}."
1724         ```
1725
1726         The following code snippet then holds:
1727         ```
1728         string_parser = StringParser()
1729         idx = string_parser.parse(line.leaves, string_idx)
1730         assert line.leaves[idx].type == token.PLUS
1731         ```
1732     """
1733
1734     DEFAULT_TOKEN = -1
1735
1736     # String Parser States
1737     START = 1
1738     DOT = 2
1739     NAME = 3
1740     PERCENT = 4
1741     SINGLE_FMT_ARG = 5
1742     LPAR = 6
1743     RPAR = 7
1744     DONE = 8
1745
1746     # Lookup Table for Next State
1747     _goto: Dict[Tuple[ParserState, NodeType], ParserState] = {
1748         # A string trailer may start with '.' OR '%'.
1749         (START, token.DOT): DOT,
1750         (START, token.PERCENT): PERCENT,
1751         (START, DEFAULT_TOKEN): DONE,
1752         # A '.' MUST be followed by an attribute or method name.
1753         (DOT, token.NAME): NAME,
1754         # A method name MUST be followed by an '(', whereas an attribute name
1755         # is the last symbol in the string trailer.
1756         (NAME, token.LPAR): LPAR,
1757         (NAME, DEFAULT_TOKEN): DONE,
1758         # A '%' symbol can be followed by an '(' or a single argument (e.g. a
1759         # string or variable name).
1760         (PERCENT, token.LPAR): LPAR,
1761         (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
1762         # If a '%' symbol is followed by a single argument, that argument is
1763         # the last leaf in the string trailer.
1764         (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
1765         # If present, a ')' symbol is the last symbol in a string trailer.
1766         # (NOTE: LPARS and nested RPARS are not included in this lookup table,
1767         # since they are treated as a special case by the parsing logic in this
1768         # classes' implementation.)
1769         (RPAR, DEFAULT_TOKEN): DONE,
1770     }
1771
1772     def __init__(self) -> None:
1773         self._state = self.START
1774         self._unmatched_lpars = 0
1775
1776     def parse(self, leaves: List[Leaf], string_idx: int) -> int:
1777         """
1778         Pre-conditions:
1779             * @leaves[@string_idx].type == token.STRING
1780
1781         Returns:
1782             The index directly after the last leaf which is apart of the string
1783             trailer, if a "trailer" exists.
1784                 OR
1785             @string_idx + 1, if no string "trailer" exists.
1786         """
1787         assert leaves[string_idx].type == token.STRING
1788
1789         idx = string_idx + 1
1790         while idx < len(leaves) and self._next_state(leaves[idx]):
1791             idx += 1
1792         return idx
1793
1794     def _next_state(self, leaf: Leaf) -> bool:
1795         """
1796         Pre-conditions:
1797             * On the first call to this function, @leaf MUST be the leaf that
1798             was directly after the string leaf in question (e.g. if our target
1799             string is `line.leaves[i]` then the first call to this method must
1800             be `line.leaves[i + 1]`).
1801             * On the next call to this function, the leaf parameter passed in
1802             MUST be the leaf directly following @leaf.
1803
1804         Returns:
1805             True iff @leaf is apart of the string's trailer.
1806         """
1807         # We ignore empty LPAR or RPAR leaves.
1808         if is_empty_par(leaf):
1809             return True
1810
1811         next_token = leaf.type
1812         if next_token == token.LPAR:
1813             self._unmatched_lpars += 1
1814
1815         current_state = self._state
1816
1817         # The LPAR parser state is a special case. We will return True until we
1818         # find the matching RPAR token.
1819         if current_state == self.LPAR:
1820             if next_token == token.RPAR:
1821                 self._unmatched_lpars -= 1
1822                 if self._unmatched_lpars == 0:
1823                     self._state = self.RPAR
1824         # Otherwise, we use a lookup table to determine the next state.
1825         else:
1826             # If the lookup table matches the current state to the next
1827             # token, we use the lookup table.
1828             if (current_state, next_token) in self._goto:
1829                 self._state = self._goto[current_state, next_token]
1830             else:
1831                 # Otherwise, we check if a the current state was assigned a
1832                 # default.
1833                 if (current_state, self.DEFAULT_TOKEN) in self._goto:
1834                     self._state = self._goto[current_state, self.DEFAULT_TOKEN]
1835                 # If no default has been assigned, then this parser has a logic
1836                 # error.
1837                 else:
1838                     raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1839
1840             if self._state == self.DONE:
1841                 return False
1842
1843         return True
1844
1845
1846 def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
1847     """
1848     Factory for a convenience function that is used to orphan @string_leaf
1849     and then insert multiple new leaves into the same part of the node
1850     structure that @string_leaf had originally occupied.
1851
1852     Examples:
1853         Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
1854         string_leaf.parent`. Assume the node `N` has the following
1855         original structure:
1856
1857         Node(
1858             expr_stmt, [
1859                 Leaf(NAME, 'x'),
1860                 Leaf(EQUAL, '='),
1861                 Leaf(STRING, '"foo"'),
1862             ]
1863         )
1864
1865         We then run the code snippet shown below.
1866         ```
1867         insert_str_child = insert_str_child_factory(string_leaf)
1868
1869         lpar = Leaf(token.LPAR, '(')
1870         insert_str_child(lpar)
1871
1872         bar = Leaf(token.STRING, '"bar"')
1873         insert_str_child(bar)
1874
1875         rpar = Leaf(token.RPAR, ')')
1876         insert_str_child(rpar)
1877         ```
1878
1879         After which point, it follows that `string_leaf.parent is None` and
1880         the node `N` now has the following structure:
1881
1882         Node(
1883             expr_stmt, [
1884                 Leaf(NAME, 'x'),
1885                 Leaf(EQUAL, '='),
1886                 Leaf(LPAR, '('),
1887                 Leaf(STRING, '"bar"'),
1888                 Leaf(RPAR, ')'),
1889             ]
1890         )
1891     """
1892     string_parent = string_leaf.parent
1893     string_child_idx = string_leaf.remove()
1894
1895     def insert_str_child(child: LN) -> None:
1896         nonlocal string_child_idx
1897
1898         assert string_parent is not None
1899         assert string_child_idx is not None
1900
1901         string_parent.insert_child(string_child_idx, child)
1902         string_child_idx += 1
1903
1904     return insert_str_child
1905
1906
1907 def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
1908     """
1909     Examples:
1910         ```
1911         my_list = [1, 2, 3]
1912
1913         is_valid_index = is_valid_index_factory(my_list)
1914
1915         assert is_valid_index(0)
1916         assert is_valid_index(2)
1917
1918         assert not is_valid_index(3)
1919         assert not is_valid_index(-1)
1920         ```
1921     """
1922
1923     def is_valid_index(idx: int) -> bool:
1924         """
1925         Returns:
1926             True iff @idx is positive AND seq[@idx] does NOT raise an
1927             IndexError.
1928         """
1929         return 0 <= idx < len(seq)
1930
1931     return is_valid_index