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

80e88a2d2fb78d834ed281256b8b3dd97511a16a
[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 (possibly prefixed by a
924         string operator [e.g. '+' or '==']), MAYBE a string trailer, and MAYBE
925         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     STRING_OPERATORS = [
956         token.PLUS,
957         token.STAR,
958         token.EQEQUAL,
959         token.NOTEQUAL,
960         token.LESS,
961         token.LESSEQUAL,
962         token.GREATER,
963         token.GREATEREQUAL,
964     ]
965     MIN_SUBSTR_SIZE = 6
966     # Matches an "f-expression" (e.g. {var}) that might be found in an f-string.
967     RE_FEXPR = r"""
968     (?<!\{) (?:\{\{)* \{ (?!\{)
969         (?:
970             [^\{\}]
971             | \{\{
972             | \}\}
973             | (?R)
974         )+?
975     (?<!\}) \} (?:\}\})* (?!\})
976     """
977
978     def do_splitter_match(self, line: Line) -> TMatchResult:
979         LL = line.leaves
980
981         is_valid_index = is_valid_index_factory(LL)
982
983         idx = 0
984
985         # The first two leaves MAY be the 'not in' keywords...
986         if (
987             is_valid_index(idx)
988             and is_valid_index(idx + 1)
989             and [LL[idx].type, LL[idx + 1].type] == [token.NAME, token.NAME]
990             and str(LL[idx]) + str(LL[idx + 1]) == "not in"
991         ):
992             idx += 2
993         # Else the first leaf MAY be a string operator symbol or the 'in' keyword...
994         elif is_valid_index(idx) and (
995             LL[idx].type in self.STRING_OPERATORS
996             or LL[idx].type == token.NAME
997             and str(LL[idx]) == "in"
998         ):
999             idx += 1
1000
1001         # The next/first leaf MAY be an empty LPAR...
1002         if is_valid_index(idx) and is_empty_lpar(LL[idx]):
1003             idx += 1
1004
1005         # The next/first leaf MUST be a string...
1006         if not is_valid_index(idx) or LL[idx].type != token.STRING:
1007             return TErr("Line does not start with a string.")
1008
1009         string_idx = idx
1010
1011         # Skip the string trailer, if one exists.
1012         string_parser = StringParser()
1013         idx = string_parser.parse(LL, string_idx)
1014
1015         # That string MAY be followed by an empty RPAR...
1016         if is_valid_index(idx) and is_empty_rpar(LL[idx]):
1017             idx += 1
1018
1019         # That string / empty RPAR leaf MAY be followed by a comma...
1020         if is_valid_index(idx) and LL[idx].type == token.COMMA:
1021             idx += 1
1022
1023         # But no more leaves are allowed...
1024         if is_valid_index(idx):
1025             return TErr("This line does not end with a string.")
1026
1027         return Ok(string_idx)
1028
1029     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1030         LL = line.leaves
1031
1032         QUOTE = LL[string_idx].value[-1]
1033
1034         is_valid_index = is_valid_index_factory(LL)
1035         insert_str_child = insert_str_child_factory(LL[string_idx])
1036
1037         prefix = get_string_prefix(LL[string_idx].value)
1038
1039         # We MAY choose to drop the 'f' prefix from substrings that don't
1040         # contain any f-expressions, but ONLY if the original f-string
1041         # contains at least one f-expression. Otherwise, we will alter the AST
1042         # of the program.
1043         drop_pointless_f_prefix = ("f" in prefix) and re.search(
1044             self.RE_FEXPR, LL[string_idx].value, re.VERBOSE
1045         )
1046
1047         first_string_line = True
1048
1049         string_op_leaves = self._get_string_operator_leaves(LL)
1050         string_op_leaves_length = (
1051             sum([len(str(prefix_leaf)) for prefix_leaf in string_op_leaves]) + 1
1052             if string_op_leaves
1053             else 0
1054         )
1055
1056         def maybe_append_string_operators(new_line: Line) -> None:
1057             """
1058             Side Effects:
1059                 If @line starts with a string operator and this is the first
1060                 line we are constructing, this function appends the string
1061                 operator to @new_line and replaces the old string operator leaf
1062                 in the node structure. Otherwise this function does nothing.
1063             """
1064             maybe_prefix_leaves = string_op_leaves if first_string_line else []
1065             for i, prefix_leaf in enumerate(maybe_prefix_leaves):
1066                 replace_child(LL[i], prefix_leaf)
1067                 new_line.append(prefix_leaf)
1068
1069         ends_with_comma = (
1070             is_valid_index(string_idx + 1) and LL[string_idx + 1].type == token.COMMA
1071         )
1072
1073         def max_last_string() -> int:
1074             """
1075             Returns:
1076                 The max allowed length of the string value used for the last
1077                 line we will construct.
1078             """
1079             result = self.line_length
1080             result -= line.depth * 4
1081             result -= 1 if ends_with_comma else 0
1082             result -= string_op_leaves_length
1083             return result
1084
1085         # --- Calculate Max Break Index (for string value)
1086         # We start with the line length limit
1087         max_break_idx = self.line_length
1088         # The last index of a string of length N is N-1.
1089         max_break_idx -= 1
1090         # Leading whitespace is not present in the string value (e.g. Leaf.value).
1091         max_break_idx -= line.depth * 4
1092         if max_break_idx < 0:
1093             yield TErr(
1094                 f"Unable to split {LL[string_idx].value} at such high of a line depth:"
1095                 f" {line.depth}"
1096             )
1097             return
1098
1099         # Check if StringMerger registered any custom splits.
1100         custom_splits = self.pop_custom_splits(LL[string_idx].value)
1101         # We use them ONLY if none of them would produce lines that exceed the
1102         # line limit.
1103         use_custom_breakpoints = bool(
1104             custom_splits
1105             and all(csplit.break_idx <= max_break_idx for csplit in custom_splits)
1106         )
1107
1108         # Temporary storage for the remaining chunk of the string line that
1109         # can't fit onto the line currently being constructed.
1110         rest_value = LL[string_idx].value
1111
1112         def more_splits_should_be_made() -> bool:
1113             """
1114             Returns:
1115                 True iff `rest_value` (the remaining string value from the last
1116                 split), should be split again.
1117             """
1118             if use_custom_breakpoints:
1119                 return len(custom_splits) > 1
1120             else:
1121                 return len(rest_value) > max_last_string()
1122
1123         string_line_results: List[Ok[Line]] = []
1124         while more_splits_should_be_made():
1125             if use_custom_breakpoints:
1126                 # Custom User Split (manual)
1127                 csplit = custom_splits.pop(0)
1128                 break_idx = csplit.break_idx
1129             else:
1130                 # Algorithmic Split (automatic)
1131                 max_bidx = max_break_idx - string_op_leaves_length
1132                 maybe_break_idx = self._get_break_idx(rest_value, max_bidx)
1133                 if maybe_break_idx is None:
1134                     # If we are unable to algorithmically determine a good split
1135                     # and this string has custom splits registered to it, we
1136                     # fall back to using them--which means we have to start
1137                     # over from the beginning.
1138                     if custom_splits:
1139                         rest_value = LL[string_idx].value
1140                         string_line_results = []
1141                         first_string_line = True
1142                         use_custom_breakpoints = True
1143                         continue
1144
1145                     # Otherwise, we stop splitting here.
1146                     break
1147
1148                 break_idx = maybe_break_idx
1149
1150             # --- Construct `next_value`
1151             next_value = rest_value[:break_idx] + QUOTE
1152             if (
1153                 # Are we allowed to try to drop a pointless 'f' prefix?
1154                 drop_pointless_f_prefix
1155                 # If we are, will we be successful?
1156                 and next_value != self._normalize_f_string(next_value, prefix)
1157             ):
1158                 # If the current custom split did NOT originally use a prefix,
1159                 # then `csplit.break_idx` will be off by one after removing
1160                 # the 'f' prefix.
1161                 break_idx = (
1162                     break_idx + 1
1163                     if use_custom_breakpoints and not csplit.has_prefix
1164                     else break_idx
1165                 )
1166                 next_value = rest_value[:break_idx] + QUOTE
1167                 next_value = self._normalize_f_string(next_value, prefix)
1168
1169             # --- Construct `next_leaf`
1170             next_leaf = Leaf(token.STRING, next_value)
1171             insert_str_child(next_leaf)
1172             self._maybe_normalize_string_quotes(next_leaf)
1173
1174             # --- Construct `next_line`
1175             next_line = line.clone()
1176             maybe_append_string_operators(next_line)
1177             next_line.append(next_leaf)
1178             string_line_results.append(Ok(next_line))
1179
1180             rest_value = prefix + QUOTE + rest_value[break_idx:]
1181             first_string_line = False
1182
1183         yield from string_line_results
1184
1185         if drop_pointless_f_prefix:
1186             rest_value = self._normalize_f_string(rest_value, prefix)
1187
1188         rest_leaf = Leaf(token.STRING, rest_value)
1189         insert_str_child(rest_leaf)
1190
1191         # NOTE: I could not find a test case that verifies that the following
1192         # line is actually necessary, but it seems to be. Otherwise we risk
1193         # not normalizing the last substring, right?
1194         self._maybe_normalize_string_quotes(rest_leaf)
1195
1196         last_line = line.clone()
1197         maybe_append_string_operators(last_line)
1198
1199         # If there are any leaves to the right of the target string...
1200         if is_valid_index(string_idx + 1):
1201             # We use `temp_value` here to determine how long the last line
1202             # would be if we were to append all the leaves to the right of the
1203             # target string to the last string line.
1204             temp_value = rest_value
1205             for leaf in LL[string_idx + 1 :]:
1206                 temp_value += str(leaf)
1207                 if leaf.type == token.LPAR:
1208                     break
1209
1210             # Try to fit them all on the same line with the last substring...
1211             if (
1212                 len(temp_value) <= max_last_string()
1213                 or LL[string_idx + 1].type == token.COMMA
1214             ):
1215                 last_line.append(rest_leaf)
1216                 append_leaves(last_line, line, LL[string_idx + 1 :])
1217                 yield Ok(last_line)
1218             # Otherwise, place the last substring on one line and everything
1219             # else on a line below that...
1220             else:
1221                 last_line.append(rest_leaf)
1222                 yield Ok(last_line)
1223
1224                 non_string_line = line.clone()
1225                 append_leaves(non_string_line, line, LL[string_idx + 1 :])
1226                 yield Ok(non_string_line)
1227         # Else the target string was the last leaf...
1228         else:
1229             last_line.append(rest_leaf)
1230             last_line.comments = line.comments.copy()
1231             yield Ok(last_line)
1232
1233     def _get_break_idx(self, string: str, max_break_idx: int) -> Optional[int]:
1234         """
1235         This method contains the algorithm that StringSplitter uses to
1236         determine which character to split each string at.
1237
1238         Args:
1239             @string: The substring that we are attempting to split.
1240             @max_break_idx: The ideal break index. We will return this value if it
1241             meets all the necessary conditions. In the likely event that it
1242             doesn't we will try to find the closest index BELOW @max_break_idx
1243             that does. If that fails, we will expand our search by also
1244             considering all valid indices ABOVE @max_break_idx.
1245
1246         Pre-Conditions:
1247             * assert_is_leaf_string(@string)
1248             * 0 <= @max_break_idx < len(@string)
1249
1250         Returns:
1251             break_idx, if an index is able to be found that meets all of the
1252             conditions listed in the 'Transformations' section of this classes'
1253             docstring.
1254                 OR
1255             None, otherwise.
1256         """
1257         is_valid_index = is_valid_index_factory(string)
1258
1259         assert is_valid_index(max_break_idx)
1260         assert_is_leaf_string(string)
1261
1262         _fexpr_slices: Optional[List[Tuple[Index, Index]]] = None
1263
1264         def fexpr_slices() -> Iterator[Tuple[Index, Index]]:
1265             """
1266             Yields:
1267                 All ranges of @string which, if @string were to be split there,
1268                 would result in the splitting of an f-expression (which is NOT
1269                 allowed).
1270             """
1271             nonlocal _fexpr_slices
1272
1273             if _fexpr_slices is None:
1274                 _fexpr_slices = []
1275                 for match in re.finditer(self.RE_FEXPR, string, re.VERBOSE):
1276                     _fexpr_slices.append(match.span())
1277
1278             yield from _fexpr_slices
1279
1280         is_fstring = "f" in get_string_prefix(string)
1281
1282         def breaks_fstring_expression(i: Index) -> bool:
1283             """
1284             Returns:
1285                 True iff returning @i would result in the splitting of an
1286                 f-expression (which is NOT allowed).
1287             """
1288             if not is_fstring:
1289                 return False
1290
1291             for (start, end) in fexpr_slices():
1292                 if start <= i < end:
1293                     return True
1294
1295             return False
1296
1297         def passes_all_checks(i: Index) -> bool:
1298             """
1299             Returns:
1300                 True iff ALL of the conditions listed in the 'Transformations'
1301                 section of this classes' docstring would be be met by returning @i.
1302             """
1303             is_space = string[i] == " "
1304
1305             is_not_escaped = True
1306             j = i - 1
1307             while is_valid_index(j) and string[j] == "\\":
1308                 is_not_escaped = not is_not_escaped
1309                 j -= 1
1310
1311             is_big_enough = (
1312                 len(string[i:]) >= self.MIN_SUBSTR_SIZE
1313                 and len(string[:i]) >= self.MIN_SUBSTR_SIZE
1314             )
1315             return (
1316                 is_space
1317                 and is_not_escaped
1318                 and is_big_enough
1319                 and not breaks_fstring_expression(i)
1320             )
1321
1322         # First, we check all indices BELOW @max_break_idx.
1323         break_idx = max_break_idx
1324         while is_valid_index(break_idx - 1) and not passes_all_checks(break_idx):
1325             break_idx -= 1
1326
1327         if not passes_all_checks(break_idx):
1328             # If that fails, we check all indices ABOVE @max_break_idx.
1329             #
1330             # If we are able to find a valid index here, the next line is going
1331             # to be longer than the specified line length, but it's probably
1332             # better than doing nothing at all.
1333             break_idx = max_break_idx + 1
1334             while is_valid_index(break_idx + 1) and not passes_all_checks(break_idx):
1335                 break_idx += 1
1336
1337             if not is_valid_index(break_idx) or not passes_all_checks(break_idx):
1338                 return None
1339
1340         return break_idx
1341
1342     def _maybe_normalize_string_quotes(self, leaf: Leaf) -> None:
1343         if self.normalize_strings:
1344             leaf.value = normalize_string_quotes(leaf.value)
1345
1346     def _normalize_f_string(self, string: str, prefix: str) -> str:
1347         """
1348         Pre-Conditions:
1349             * assert_is_leaf_string(@string)
1350
1351         Returns:
1352             * If @string is an f-string that contains no f-expressions, we
1353             return a string identical to @string except that the 'f' prefix
1354             has been stripped and all double braces (i.e. '{{' or '}}') have
1355             been normalized (i.e. turned into '{' or '}').
1356                 OR
1357             * Otherwise, we return @string.
1358         """
1359         assert_is_leaf_string(string)
1360
1361         if "f" in prefix and not re.search(self.RE_FEXPR, string, re.VERBOSE):
1362             new_prefix = prefix.replace("f", "")
1363
1364             temp = string[len(prefix) :]
1365             temp = re.sub(r"\{\{", "{", temp)
1366             temp = re.sub(r"\}\}", "}", temp)
1367             new_string = temp
1368
1369             return f"{new_prefix}{new_string}"
1370         else:
1371             return string
1372
1373     def _get_string_operator_leaves(self, leaves: Iterable[Leaf]) -> List[Leaf]:
1374         LL = list(leaves)
1375
1376         string_op_leaves = []
1377         i = 0
1378         while LL[i].type in self.STRING_OPERATORS + [token.NAME]:
1379             prefix_leaf = Leaf(LL[i].type, str(LL[i]).strip())
1380             string_op_leaves.append(prefix_leaf)
1381             i += 1
1382         return string_op_leaves
1383
1384
1385 class StringParenWrapper(CustomSplitMapMixin, BaseStringSplitter):
1386     """
1387     StringTransformer that splits non-"atom" strings (i.e. strings that do not
1388     exist on lines by themselves).
1389
1390     Requirements:
1391         All of the requirements listed in BaseStringSplitter's docstring in
1392         addition to the requirements listed below:
1393
1394         * The line is a return/yield statement, which returns/yields a string.
1395             OR
1396         * The line is part of a ternary expression (e.g. `x = y if cond else
1397         z`) such that the line starts with `else <string>`, where <string> is
1398         some string.
1399             OR
1400         * The line is an assert statement, which ends with a string.
1401             OR
1402         * The line is an assignment statement (e.g. `x = <string>` or `x +=
1403         <string>`) such that the variable is being assigned the value of some
1404         string.
1405             OR
1406         * The line is a dictionary key assignment where some valid key is being
1407         assigned the value of some string.
1408
1409     Transformations:
1410         The chosen string is wrapped in parentheses and then split at the LPAR.
1411
1412         We then have one line which ends with an LPAR and another line that
1413         starts with the chosen string. The latter line is then split again at
1414         the RPAR. This results in the RPAR (and possibly a trailing comma)
1415         being placed on its own line.
1416
1417         NOTE: If any leaves exist to the right of the chosen string (except
1418         for a trailing comma, which would be placed after the RPAR), those
1419         leaves are placed inside the parentheses.  In effect, the chosen
1420         string is not necessarily being "wrapped" by parentheses. We can,
1421         however, count on the LPAR being placed directly before the chosen
1422         string.
1423
1424         In other words, StringParenWrapper creates "atom" strings. These
1425         can then be split again by StringSplitter, if necessary.
1426
1427     Collaborations:
1428         In the event that a string line split by StringParenWrapper is
1429         changed such that it no longer needs to be given its own line,
1430         StringParenWrapper relies on StringParenStripper to clean up the
1431         parentheses it created.
1432     """
1433
1434     def do_splitter_match(self, line: Line) -> TMatchResult:
1435         LL = line.leaves
1436
1437         if line.leaves[-1].type in OPENING_BRACKETS:
1438             return TErr(
1439                 "Cannot wrap parens around a line that ends in an opening bracket."
1440             )
1441
1442         string_idx = (
1443             self._return_match(LL)
1444             or self._else_match(LL)
1445             or self._assert_match(LL)
1446             or self._assign_match(LL)
1447             or self._dict_match(LL)
1448         )
1449
1450         if string_idx is not None:
1451             string_value = line.leaves[string_idx].value
1452             # If the string has no spaces...
1453             if " " not in string_value:
1454                 # And will still violate the line length limit when split...
1455                 max_string_length = self.line_length - ((line.depth + 1) * 4)
1456                 if len(string_value) > max_string_length:
1457                     # And has no associated custom splits...
1458                     if not self.has_custom_splits(string_value):
1459                         # Then we should NOT put this string on its own line.
1460                         return TErr(
1461                             "We do not wrap long strings in parentheses when the"
1462                             " resultant line would still be over the specified line"
1463                             " length and can't be split further by StringSplitter."
1464                         )
1465             return Ok(string_idx)
1466
1467         return TErr("This line does not contain any non-atomic strings.")
1468
1469     @staticmethod
1470     def _return_match(LL: List[Leaf]) -> Optional[int]:
1471         """
1472         Returns:
1473             string_idx such that @LL[string_idx] is equal to our target (i.e.
1474             matched) string, if this line matches the return/yield statement
1475             requirements listed in the 'Requirements' section of this classes'
1476             docstring.
1477                 OR
1478             None, otherwise.
1479         """
1480         # If this line is apart of a return/yield statement and the first leaf
1481         # contains either the "return" or "yield" keywords...
1482         if parent_type(LL[0]) in [syms.return_stmt, syms.yield_expr] and LL[
1483             0
1484         ].value in ["return", "yield"]:
1485             is_valid_index = is_valid_index_factory(LL)
1486
1487             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1488             # The next visible leaf MUST contain a string...
1489             if is_valid_index(idx) and LL[idx].type == token.STRING:
1490                 return idx
1491
1492         return None
1493
1494     @staticmethod
1495     def _else_match(LL: List[Leaf]) -> Optional[int]:
1496         """
1497         Returns:
1498             string_idx such that @LL[string_idx] is equal to our target (i.e.
1499             matched) string, if this line matches the ternary expression
1500             requirements listed in the 'Requirements' section of this classes'
1501             docstring.
1502                 OR
1503             None, otherwise.
1504         """
1505         # If this line is apart of a ternary expression and the first leaf
1506         # contains the "else" keyword...
1507         if (
1508             parent_type(LL[0]) == syms.test
1509             and LL[0].type == token.NAME
1510             and LL[0].value == "else"
1511         ):
1512             is_valid_index = is_valid_index_factory(LL)
1513
1514             idx = 2 if is_valid_index(1) and is_empty_par(LL[1]) else 1
1515             # The next visible leaf MUST contain a string...
1516             if is_valid_index(idx) and LL[idx].type == token.STRING:
1517                 return idx
1518
1519         return None
1520
1521     @staticmethod
1522     def _assert_match(LL: List[Leaf]) -> Optional[int]:
1523         """
1524         Returns:
1525             string_idx such that @LL[string_idx] is equal to our target (i.e.
1526             matched) string, if this line matches the assert statement
1527             requirements listed in the 'Requirements' section of this classes'
1528             docstring.
1529                 OR
1530             None, otherwise.
1531         """
1532         # If this line is apart of an assert statement and the first leaf
1533         # contains the "assert" keyword...
1534         if parent_type(LL[0]) == syms.assert_stmt and LL[0].value == "assert":
1535             is_valid_index = is_valid_index_factory(LL)
1536
1537             for (i, leaf) in enumerate(LL):
1538                 # We MUST find a comma...
1539                 if leaf.type == token.COMMA:
1540                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1541
1542                     # That comma MUST be followed by a string...
1543                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1544                         string_idx = idx
1545
1546                         # Skip the string trailer, if one exists.
1547                         string_parser = StringParser()
1548                         idx = string_parser.parse(LL, string_idx)
1549
1550                         # But no more leaves are allowed...
1551                         if not is_valid_index(idx):
1552                             return string_idx
1553
1554         return None
1555
1556     @staticmethod
1557     def _assign_match(LL: List[Leaf]) -> Optional[int]:
1558         """
1559         Returns:
1560             string_idx such that @LL[string_idx] is equal to our target (i.e.
1561             matched) string, if this line matches the assignment statement
1562             requirements listed in the 'Requirements' section of this classes'
1563             docstring.
1564                 OR
1565             None, otherwise.
1566         """
1567         # If this line is apart of an expression statement or is a function
1568         # argument AND the first leaf contains a variable name...
1569         if (
1570             parent_type(LL[0]) in [syms.expr_stmt, syms.argument, syms.power]
1571             and LL[0].type == token.NAME
1572         ):
1573             is_valid_index = is_valid_index_factory(LL)
1574
1575             for (i, leaf) in enumerate(LL):
1576                 # We MUST find either an '=' or '+=' symbol...
1577                 if leaf.type in [token.EQUAL, token.PLUSEQUAL]:
1578                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1579
1580                     # That symbol MUST be followed by a string...
1581                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1582                         string_idx = idx
1583
1584                         # Skip the string trailer, if one exists.
1585                         string_parser = StringParser()
1586                         idx = string_parser.parse(LL, string_idx)
1587
1588                         # The next leaf MAY be a comma iff this line is apart
1589                         # of a function argument...
1590                         if (
1591                             parent_type(LL[0]) == syms.argument
1592                             and is_valid_index(idx)
1593                             and LL[idx].type == token.COMMA
1594                         ):
1595                             idx += 1
1596
1597                         # But no more leaves are allowed...
1598                         if not is_valid_index(idx):
1599                             return string_idx
1600
1601         return None
1602
1603     @staticmethod
1604     def _dict_match(LL: List[Leaf]) -> Optional[int]:
1605         """
1606         Returns:
1607             string_idx such that @LL[string_idx] is equal to our target (i.e.
1608             matched) string, if this line matches the dictionary key assignment
1609             statement requirements listed in the 'Requirements' section of this
1610             classes' docstring.
1611                 OR
1612             None, otherwise.
1613         """
1614         # If this line is apart of a dictionary key assignment...
1615         if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
1616             is_valid_index = is_valid_index_factory(LL)
1617
1618             for (i, leaf) in enumerate(LL):
1619                 # We MUST find a colon...
1620                 if leaf.type == token.COLON:
1621                     idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1
1622
1623                     # That colon MUST be followed by a string...
1624                     if is_valid_index(idx) and LL[idx].type == token.STRING:
1625                         string_idx = idx
1626
1627                         # Skip the string trailer, if one exists.
1628                         string_parser = StringParser()
1629                         idx = string_parser.parse(LL, string_idx)
1630
1631                         # That string MAY be followed by a comma...
1632                         if is_valid_index(idx) and LL[idx].type == token.COMMA:
1633                             idx += 1
1634
1635                         # But no more leaves are allowed...
1636                         if not is_valid_index(idx):
1637                             return string_idx
1638
1639         return None
1640
1641     def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
1642         LL = line.leaves
1643
1644         is_valid_index = is_valid_index_factory(LL)
1645         insert_str_child = insert_str_child_factory(LL[string_idx])
1646
1647         comma_idx = -1
1648         ends_with_comma = False
1649         if LL[comma_idx].type == token.COMMA:
1650             ends_with_comma = True
1651
1652         leaves_to_steal_comments_from = [LL[string_idx]]
1653         if ends_with_comma:
1654             leaves_to_steal_comments_from.append(LL[comma_idx])
1655
1656         # --- First Line
1657         first_line = line.clone()
1658         left_leaves = LL[:string_idx]
1659
1660         # We have to remember to account for (possibly invisible) LPAR and RPAR
1661         # leaves that already wrapped the target string. If these leaves do
1662         # exist, we will replace them with our own LPAR and RPAR leaves.
1663         old_parens_exist = False
1664         if left_leaves and left_leaves[-1].type == token.LPAR:
1665             old_parens_exist = True
1666             leaves_to_steal_comments_from.append(left_leaves[-1])
1667             left_leaves.pop()
1668
1669         append_leaves(first_line, line, left_leaves)
1670
1671         lpar_leaf = Leaf(token.LPAR, "(")
1672         if old_parens_exist:
1673             replace_child(LL[string_idx - 1], lpar_leaf)
1674         else:
1675             insert_str_child(lpar_leaf)
1676         first_line.append(lpar_leaf)
1677
1678         # We throw inline comments that were originally to the right of the
1679         # target string to the top line. They will now be shown to the right of
1680         # the LPAR.
1681         for leaf in leaves_to_steal_comments_from:
1682             for comment_leaf in line.comments_after(leaf):
1683                 first_line.append(comment_leaf, preformatted=True)
1684
1685         yield Ok(first_line)
1686
1687         # --- Middle (String) Line
1688         # We only need to yield one (possibly too long) string line, since the
1689         # `StringSplitter` will break it down further if necessary.
1690         string_value = LL[string_idx].value
1691         string_line = Line(
1692             mode=line.mode,
1693             depth=line.depth + 1,
1694             inside_brackets=True,
1695             should_split_rhs=line.should_split_rhs,
1696             magic_trailing_comma=line.magic_trailing_comma,
1697         )
1698         string_leaf = Leaf(token.STRING, string_value)
1699         insert_str_child(string_leaf)
1700         string_line.append(string_leaf)
1701
1702         old_rpar_leaf = None
1703         if is_valid_index(string_idx + 1):
1704             right_leaves = LL[string_idx + 1 :]
1705             if ends_with_comma:
1706                 right_leaves.pop()
1707
1708             if old_parens_exist:
1709                 assert right_leaves and right_leaves[-1].type == token.RPAR, (
1710                     "Apparently, old parentheses do NOT exist?!"
1711                     f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
1712                 )
1713                 old_rpar_leaf = right_leaves.pop()
1714
1715             append_leaves(string_line, line, right_leaves)
1716
1717         yield Ok(string_line)
1718
1719         # --- Last Line
1720         last_line = line.clone()
1721         last_line.bracket_tracker = first_line.bracket_tracker
1722
1723         new_rpar_leaf = Leaf(token.RPAR, ")")
1724         if old_rpar_leaf is not None:
1725             replace_child(old_rpar_leaf, new_rpar_leaf)
1726         else:
1727             insert_str_child(new_rpar_leaf)
1728         last_line.append(new_rpar_leaf)
1729
1730         # If the target string ended with a comma, we place this comma to the
1731         # right of the RPAR on the last line.
1732         if ends_with_comma:
1733             comma_leaf = Leaf(token.COMMA, ",")
1734             replace_child(LL[comma_idx], comma_leaf)
1735             last_line.append(comma_leaf)
1736
1737         yield Ok(last_line)
1738
1739
1740 class StringParser:
1741     """
1742     A state machine that aids in parsing a string's "trailer", which can be
1743     either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
1744     (varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
1745     varY)`).
1746
1747     NOTE: A new StringParser object MUST be instantiated for each string
1748     trailer we need to parse.
1749
1750     Examples:
1751         We shall assume that `line` equals the `Line` object that corresponds
1752         to the following line of python code:
1753         ```
1754         x = "Some {}.".format("String") + some_other_string
1755         ```
1756
1757         Furthermore, we will assume that `string_idx` is some index such that:
1758         ```
1759         assert line.leaves[string_idx].value == "Some {}."
1760         ```
1761
1762         The following code snippet then holds:
1763         ```
1764         string_parser = StringParser()
1765         idx = string_parser.parse(line.leaves, string_idx)
1766         assert line.leaves[idx].type == token.PLUS
1767         ```
1768     """
1769
1770     DEFAULT_TOKEN = -1
1771
1772     # String Parser States
1773     START = 1
1774     DOT = 2
1775     NAME = 3
1776     PERCENT = 4
1777     SINGLE_FMT_ARG = 5
1778     LPAR = 6
1779     RPAR = 7
1780     DONE = 8
1781
1782     # Lookup Table for Next State
1783     _goto: Dict[Tuple[ParserState, NodeType], ParserState] = {
1784         # A string trailer may start with '.' OR '%'.
1785         (START, token.DOT): DOT,
1786         (START, token.PERCENT): PERCENT,
1787         (START, DEFAULT_TOKEN): DONE,
1788         # A '.' MUST be followed by an attribute or method name.
1789         (DOT, token.NAME): NAME,
1790         # A method name MUST be followed by an '(', whereas an attribute name
1791         # is the last symbol in the string trailer.
1792         (NAME, token.LPAR): LPAR,
1793         (NAME, DEFAULT_TOKEN): DONE,
1794         # A '%' symbol can be followed by an '(' or a single argument (e.g. a
1795         # string or variable name).
1796         (PERCENT, token.LPAR): LPAR,
1797         (PERCENT, DEFAULT_TOKEN): SINGLE_FMT_ARG,
1798         # If a '%' symbol is followed by a single argument, that argument is
1799         # the last leaf in the string trailer.
1800         (SINGLE_FMT_ARG, DEFAULT_TOKEN): DONE,
1801         # If present, a ')' symbol is the last symbol in a string trailer.
1802         # (NOTE: LPARS and nested RPARS are not included in this lookup table,
1803         # since they are treated as a special case by the parsing logic in this
1804         # classes' implementation.)
1805         (RPAR, DEFAULT_TOKEN): DONE,
1806     }
1807
1808     def __init__(self) -> None:
1809         self._state = self.START
1810         self._unmatched_lpars = 0
1811
1812     def parse(self, leaves: List[Leaf], string_idx: int) -> int:
1813         """
1814         Pre-conditions:
1815             * @leaves[@string_idx].type == token.STRING
1816
1817         Returns:
1818             The index directly after the last leaf which is apart of the string
1819             trailer, if a "trailer" exists.
1820                 OR
1821             @string_idx + 1, if no string "trailer" exists.
1822         """
1823         assert leaves[string_idx].type == token.STRING
1824
1825         idx = string_idx + 1
1826         while idx < len(leaves) and self._next_state(leaves[idx]):
1827             idx += 1
1828         return idx
1829
1830     def _next_state(self, leaf: Leaf) -> bool:
1831         """
1832         Pre-conditions:
1833             * On the first call to this function, @leaf MUST be the leaf that
1834             was directly after the string leaf in question (e.g. if our target
1835             string is `line.leaves[i]` then the first call to this method must
1836             be `line.leaves[i + 1]`).
1837             * On the next call to this function, the leaf parameter passed in
1838             MUST be the leaf directly following @leaf.
1839
1840         Returns:
1841             True iff @leaf is apart of the string's trailer.
1842         """
1843         # We ignore empty LPAR or RPAR leaves.
1844         if is_empty_par(leaf):
1845             return True
1846
1847         next_token = leaf.type
1848         if next_token == token.LPAR:
1849             self._unmatched_lpars += 1
1850
1851         current_state = self._state
1852
1853         # The LPAR parser state is a special case. We will return True until we
1854         # find the matching RPAR token.
1855         if current_state == self.LPAR:
1856             if next_token == token.RPAR:
1857                 self._unmatched_lpars -= 1
1858                 if self._unmatched_lpars == 0:
1859                     self._state = self.RPAR
1860         # Otherwise, we use a lookup table to determine the next state.
1861         else:
1862             # If the lookup table matches the current state to the next
1863             # token, we use the lookup table.
1864             if (current_state, next_token) in self._goto:
1865                 self._state = self._goto[current_state, next_token]
1866             else:
1867                 # Otherwise, we check if a the current state was assigned a
1868                 # default.
1869                 if (current_state, self.DEFAULT_TOKEN) in self._goto:
1870                     self._state = self._goto[current_state, self.DEFAULT_TOKEN]
1871                 # If no default has been assigned, then this parser has a logic
1872                 # error.
1873                 else:
1874                     raise RuntimeError(f"{self.__class__.__name__} LOGIC ERROR!")
1875
1876             if self._state == self.DONE:
1877                 return False
1878
1879         return True
1880
1881
1882 def insert_str_child_factory(string_leaf: Leaf) -> Callable[[LN], None]:
1883     """
1884     Factory for a convenience function that is used to orphan @string_leaf
1885     and then insert multiple new leaves into the same part of the node
1886     structure that @string_leaf had originally occupied.
1887
1888     Examples:
1889         Let `string_leaf = Leaf(token.STRING, '"foo"')` and `N =
1890         string_leaf.parent`. Assume the node `N` has the following
1891         original structure:
1892
1893         Node(
1894             expr_stmt, [
1895                 Leaf(NAME, 'x'),
1896                 Leaf(EQUAL, '='),
1897                 Leaf(STRING, '"foo"'),
1898             ]
1899         )
1900
1901         We then run the code snippet shown below.
1902         ```
1903         insert_str_child = insert_str_child_factory(string_leaf)
1904
1905         lpar = Leaf(token.LPAR, '(')
1906         insert_str_child(lpar)
1907
1908         bar = Leaf(token.STRING, '"bar"')
1909         insert_str_child(bar)
1910
1911         rpar = Leaf(token.RPAR, ')')
1912         insert_str_child(rpar)
1913         ```
1914
1915         After which point, it follows that `string_leaf.parent is None` and
1916         the node `N` now has the following structure:
1917
1918         Node(
1919             expr_stmt, [
1920                 Leaf(NAME, 'x'),
1921                 Leaf(EQUAL, '='),
1922                 Leaf(LPAR, '('),
1923                 Leaf(STRING, '"bar"'),
1924                 Leaf(RPAR, ')'),
1925             ]
1926         )
1927     """
1928     string_parent = string_leaf.parent
1929     string_child_idx = string_leaf.remove()
1930
1931     def insert_str_child(child: LN) -> None:
1932         nonlocal string_child_idx
1933
1934         assert string_parent is not None
1935         assert string_child_idx is not None
1936
1937         string_parent.insert_child(string_child_idx, child)
1938         string_child_idx += 1
1939
1940     return insert_str_child
1941
1942
1943 def is_valid_index_factory(seq: Sequence[Any]) -> Callable[[int], bool]:
1944     """
1945     Examples:
1946         ```
1947         my_list = [1, 2, 3]
1948
1949         is_valid_index = is_valid_index_factory(my_list)
1950
1951         assert is_valid_index(0)
1952         assert is_valid_index(2)
1953
1954         assert not is_valid_index(3)
1955         assert not is_valid_index(-1)
1956         ```
1957     """
1958
1959     def is_valid_index(idx: int) -> bool:
1960         """
1961         Returns:
1962             True iff @idx is positive AND seq[@idx] does NOT raise an
1963             IndexError.
1964         """
1965         return 0 <= idx < len(seq)
1966
1967     return is_valid_index