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

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