prettycsharp 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,732 @@
1
+ """Syntax rewrites: the changes that move or replace tokens.
2
+
3
+ Each rewrite edits the syntax model in place before printing. A token that
4
+ is removed hands its comments to a neighbour, so no comment is lost; the
5
+ verifier checks that every rewrite left the code meaning the same.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable, Sequence
11
+
12
+ from prettycsharp.settings import Settings
13
+ from prettycsharp.syntax import Comment, Node, SourceFile, token, walk
14
+ from prettycsharp.verify import is_embedded
15
+
16
+ _MODIFIER_OWNERS = frozenset(
17
+ {
18
+ "class_declaration",
19
+ "struct_declaration",
20
+ "interface_declaration",
21
+ "record_declaration",
22
+ "enum_declaration",
23
+ "delegate_declaration",
24
+ "method_declaration",
25
+ "constructor_declaration",
26
+ "destructor_declaration",
27
+ "operator_declaration",
28
+ "conversion_operator_declaration",
29
+ "property_declaration",
30
+ "indexer_declaration",
31
+ "event_declaration",
32
+ "event_field_declaration",
33
+ "field_declaration",
34
+ "local_function_statement",
35
+ }
36
+ )
37
+
38
+ # Declarations whose attribute lists go one attribute per list.
39
+ _ATTRIBUTE_OWNERS = _MODIFIER_OWNERS | {"accessor_declaration", "enum_member_declaration", "compilation_unit"}
40
+
41
+ _BODY_OWNERS = frozenset(
42
+ {
43
+ "method_declaration",
44
+ "constructor_declaration",
45
+ "destructor_declaration",
46
+ "operator_declaration",
47
+ "conversion_operator_declaration",
48
+ "local_function_statement",
49
+ }
50
+ )
51
+
52
+ _LIST_WITH_TRAILING_COMMA = frozenset(
53
+ {
54
+ "initializer_expression",
55
+ "collection_expression",
56
+ "enum_member_declaration_list",
57
+ "anonymous_object_creation_expression",
58
+ "switch_expression",
59
+ "with_expression",
60
+ "list_pattern",
61
+ "property_pattern_clause",
62
+ }
63
+ )
64
+
65
+ _LITERALS = frozenset(
66
+ {
67
+ "integer_literal",
68
+ "real_literal",
69
+ "string_literal",
70
+ "verbatim_string_literal",
71
+ "raw_string_literal",
72
+ "character_literal",
73
+ "boolean_literal",
74
+ "null_literal",
75
+ }
76
+ )
77
+
78
+
79
+ def rewrite(file: SourceFile, settings: Settings, lang_version: float) -> None:
80
+ root = file.root
81
+ if settings.namespace_style != "preserve":
82
+ target = settings.namespace_style
83
+ if target == "file_scoped_if_supported":
84
+ target = "file_scoped" if lang_version >= 10 else "block"
85
+ if target == "file_scoped":
86
+ to_file_scoped(file)
87
+ else:
88
+ to_block_scoped(file)
89
+ if settings.using_order != "preserve":
90
+ sort_usings(root, settings.using_order)
91
+ if settings.modifiers is not None:
92
+ order_modifiers(root, settings.modifiers)
93
+ if settings.attributes == "separate":
94
+ separate_attributes(root)
95
+ elif settings.attributes == "combine":
96
+ combine_attributes(root)
97
+ if settings.expression_bodies != "preserve":
98
+ expression_bodies(root, settings.expression_bodies, lang_version)
99
+ if settings.parentheses != "preserve":
100
+ remove_parentheses(root, settings.parentheses)
101
+ if settings.braces == "always":
102
+ add_braces(root)
103
+ if settings.trailing_commas == "remove":
104
+ remove_trailing_commas(root)
105
+
106
+
107
+ # Trivia ------------------------------------------------------------------
108
+
109
+
110
+ def _frozen(node: Node) -> bool:
111
+ current: Node | None = node
112
+ while current is not None:
113
+ if current.verbatim:
114
+ return True
115
+ current = current.parent
116
+ return False
117
+
118
+
119
+ def _has_comments(nodes: Iterable[Node]) -> bool:
120
+ return any(t.leading or t.trailing for n in nodes for t in n.tokens())
121
+
122
+
123
+ def _has_verbatim(node: Node) -> bool:
124
+ """Whether any part of the node is printed as written, and so must not move."""
125
+ return any(n.verbatim for n in walk(node))
126
+
127
+
128
+ def _set_children(node: Node, children: list[Node]) -> None:
129
+ node.children = children
130
+ for child in children:
131
+ child.parent = node
132
+
133
+
134
+ def _as_leading(comments: Sequence[Comment]) -> list[Comment]:
135
+ for comment in comments:
136
+ comment.own_line = True
137
+ comment.newline_after = True
138
+ comment.newlines_before = max(comment.newlines_before, 1)
139
+ return list(comments)
140
+
141
+
142
+ # Namespaces --------------------------------------------------------------
143
+
144
+
145
+ def to_file_scoped(file: SourceFile) -> None:
146
+ root = file.root
147
+ children = root.children
148
+ namespaces = [c for c in children if c.type == "namespace_declaration"]
149
+ if len(namespaces) != 1 or any(c.type == "file_scoped_namespace_declaration" for c in children):
150
+ return
151
+ namespace = namespaces[0]
152
+ if _frozen(namespace):
153
+ return
154
+ index = children.index(namespace)
155
+ allowed = ("using_directive", "extern_alias_directive", "global_attribute")
156
+ if any(c.type not in allowed and not c.type.startswith("preproc") for c in children[:index]):
157
+ return
158
+ if children[index + 1 :]:
159
+ return
160
+ body = namespace.child("body")
161
+ if body is None or body.type != "declaration_list" or len(body.children) < 2:
162
+ return
163
+ head = [c for c in namespace.children if c is not body]
164
+ if head[-1:] and head[-1].text == ";":
165
+ return
166
+ members = body.children[1:-1]
167
+ if any(n.type in ("namespace_declaration", "file_scoped_namespace_declaration") for m in members for n in walk(m)):
168
+ return
169
+ open_tok, close_tok = body.children[0], body.children[-1]
170
+ semicolon = token(";")
171
+ # Comments around the braces move: after { to the first member, before
172
+ # and after } to the end of the file.
173
+ moved = _as_leading([*open_tok.leading, *open_tok.trailing])
174
+ first = members[0].first_token() if members else None
175
+ if first is not None:
176
+ first.leading[:0] = moved
177
+ else:
178
+ file.trailing_comments[:0] = moved
179
+ file.trailing_comments[:0] = _as_leading([*close_tok.leading, *close_tok.trailing])
180
+ scoped = Node(type="file_scoped_namespace_declaration")
181
+ _set_children(scoped, [*head, semicolon])
182
+ _set_children(root, [*children[:index], scoped, *members])
183
+
184
+
185
+ def to_block_scoped(file: SourceFile) -> None:
186
+ root = file.root
187
+ children = root.children
188
+ scoped = next((c for c in children if c.type == "file_scoped_namespace_declaration"), None)
189
+ if scoped is None or _frozen(scoped) or not scoped.children or scoped.children[-1].text != ";":
190
+ return
191
+ index = children.index(scoped)
192
+ members = children[index + 1 :]
193
+ semicolon = scoped.children[-1]
194
+ open_tok = token("{")
195
+ close_tok = token("}")
196
+ open_tok.trailing = list(semicolon.trailing)
197
+ open_tok.leading = list(semicolon.leading)
198
+ close_tok.leading = _as_leading(file.trailing_comments)
199
+ file.trailing_comments = []
200
+ body = Node(type="declaration_list", field="body")
201
+ _set_children(body, [open_tok, *members, close_tok])
202
+ namespace = Node(type="namespace_declaration")
203
+ _set_children(namespace, [*scoped.children[:-1], body])
204
+ _set_children(root, [*children[:index], namespace])
205
+
206
+
207
+ # Usings ----------------------------------------------------------------
208
+
209
+
210
+ def using_key(node: Node, order: str) -> tuple[int, int, int, str]:
211
+ texts = [t.text or "" for t in node.tokens()]
212
+ is_global = 0 if texts[:1] == ["global"] else 1
213
+ words = [w for w in texts if w not in ("global", "using", ";")]
214
+ if words[:1] == ["static"]:
215
+ kind, words = 1, words[1:]
216
+ elif "=" in words:
217
+ kind = 2
218
+ else:
219
+ kind = 0
220
+ if words[:1] == ["unsafe"]:
221
+ words = words[1:]
222
+ name = "".join(words)
223
+ system = 1
224
+ if order in ("system_first", "system_first_grouped") and (name == "System" or name.startswith("System.")):
225
+ system = 0
226
+ return (is_global, kind, system, name)
227
+
228
+
229
+ def sort_usings(root: Node, order: str) -> None:
230
+ for node in walk(root):
231
+ if node.type not in ("compilation_unit", "declaration_list") or _frozen(node):
232
+ continue
233
+ children = node.children
234
+ i = 0
235
+ while i < len(children):
236
+ if children[i].type != "using_directive":
237
+ i += 1
238
+ continue
239
+ end = i
240
+ while end < len(children) and children[end].type == "using_directive":
241
+ end += 1
242
+ run = children[i:end]
243
+ ordered = sorted(run, key=lambda n: using_key(n, order))
244
+ if ordered != run:
245
+ _keep_header(run, ordered)
246
+ children[i:end] = ordered
247
+ i = end
248
+
249
+
250
+ def _keep_header(run: Sequence[Node], ordered: Sequence[Node]) -> None:
251
+ """The comments above the first item stay above the first item."""
252
+ old = run[0].first_token()
253
+ new = ordered[0].first_token()
254
+ if old is None or new is None or old is new:
255
+ return
256
+ header = old.leading
257
+ old.leading = []
258
+ new.leading[:0] = header
259
+ old.newlines_before, new.newlines_before = new.newlines_before, old.newlines_before
260
+
261
+
262
+ # Modifiers ---------------------------------------------------------------
263
+
264
+
265
+ def modifier_rank(text: str, order: Sequence[str]) -> float:
266
+ if text == "partial":
267
+ return 1000
268
+ if text == "ref":
269
+ return 999
270
+ if text == "const":
271
+ text = "static"
272
+ if text in order:
273
+ return order.index(text)
274
+ return 900
275
+
276
+
277
+ def order_modifiers(root: Node, order: Sequence[str]) -> None:
278
+ for node in walk(root):
279
+ if node.type not in _MODIFIER_OWNERS or _frozen(node):
280
+ continue
281
+ indexes = [i for i, c in enumerate(node.children) if c.type == "modifier"]
282
+ if len(indexes) < 2 or indexes[-1] - indexes[0] != len(indexes) - 1:
283
+ continue
284
+ run = [node.children[i] for i in indexes]
285
+ ordered = sorted(run, key=lambda m: modifier_rank(_token_text(m), order))
286
+ if ordered != run:
287
+ _keep_header(run, ordered)
288
+ node.children[indexes[0] : indexes[-1] + 1] = ordered
289
+
290
+
291
+ # Attributes --------------------------------------------------------------
292
+
293
+
294
+ def separate_attributes(root: Node) -> None:
295
+ for node in list(walk(root)):
296
+ if node.type not in _ATTRIBUTE_OWNERS or _frozen(node):
297
+ continue
298
+ children: list[Node] = []
299
+ changed = False
300
+ for child in node.children:
301
+ if child.type in ("attribute_list", "global_attribute"):
302
+ pieces = _split_list(child)
303
+ changed = changed or len(pieces) > 1
304
+ children.extend(pieces)
305
+ else:
306
+ children.append(child)
307
+ if changed:
308
+ _set_children(node, children)
309
+
310
+
311
+ def _split_list(node: Node) -> list[Node]:
312
+ kids = node.children
313
+ attributes = [c for c in kids if c.type == "attribute"]
314
+ if len(attributes) < 2 or kids[0].text != "[" or kids[-1].text != "]":
315
+ return [node]
316
+ if any(c.type.startswith("preproc") for c in kids):
317
+ return [node]
318
+ target = [c for c in kids[1:-1] if c.type not in ("attribute",) and c.text != ","]
319
+ commas = [c for c in kids if c.text == "," and not c.named]
320
+ # A comma's comments go to the attribute before it.
321
+ for comma, attribute in zip(commas, attributes, strict=False):
322
+ last = attribute.last_token()
323
+ if last is not None:
324
+ last.trailing.extend(comma.trailing)
325
+ last.trailing.extend(comma.leading)
326
+ pieces: list[Node] = []
327
+ for i, attribute in enumerate(attributes):
328
+ open_tok = kids[0] if i == 0 else token("[")
329
+ close_tok = kids[-1] if i == len(attributes) - 1 else token("]")
330
+ piece_target = target if i == 0 else [_copy_tokens(t) for t in target]
331
+ piece = Node(type=node.type, field=node.field)
332
+ _set_children(piece, [open_tok, *piece_target, attribute, close_tok])
333
+ pieces.append(piece)
334
+ return pieces
335
+
336
+
337
+ def _copy_tokens(node: Node) -> Node:
338
+ if node.text is not None:
339
+ return Node(type=node.type, named=node.named, text=node.text, field=node.field)
340
+ copy = Node(type=node.type, named=node.named, field=node.field)
341
+ _set_children(copy, [_copy_tokens(c) for c in node.children])
342
+ return copy
343
+
344
+
345
+ def combine_attributes(root: Node) -> None:
346
+ for node in list(walk(root)):
347
+ if node.type not in _ATTRIBUTE_OWNERS or _frozen(node):
348
+ continue
349
+ children: list[Node] = []
350
+ for child in node.children:
351
+ previous = children[-1] if children else None
352
+ if (
353
+ child.type == "attribute_list"
354
+ and previous is not None
355
+ and previous.type == "attribute_list"
356
+ and _target_text(previous) == _target_text(child)
357
+ and not _has_comments([child])
358
+ and not any(c.type.startswith("preproc") for c in child.children + previous.children)
359
+ ):
360
+ attributes = [c for c in child.children if c.type == "attribute"]
361
+ merged = previous.children[:-1]
362
+ for attribute in attributes:
363
+ merged.extend([token(","), attribute])
364
+ merged.append(previous.children[-1])
365
+ _set_children(previous, merged)
366
+ else:
367
+ children.append(child)
368
+ _set_children(node, children)
369
+
370
+
371
+ def _target_text(node: Node) -> str:
372
+ target = node.first_of("attribute_target_specifier")
373
+ return "".join(t.text or "" for t in target.tokens()) if target is not None else ""
374
+
375
+
376
+ # Expression bodies -------------------------------------------------------
377
+
378
+
379
+ def expression_bodies(root: Node, mode: str, lang_version: float) -> None:
380
+ for node in list(walk(root)):
381
+ if _frozen(node):
382
+ continue
383
+ if node.type in _BODY_OWNERS:
384
+ if mode == "prefer":
385
+ if node.type in ("constructor_declaration", "destructor_declaration") and lang_version < 7:
386
+ continue
387
+ if lang_version >= 6:
388
+ _block_to_arrow(node)
389
+ elif mode in ("block", "properties_only"):
390
+ _arrow_to_block(node, _returns_value(node))
391
+ elif node.type in ("property_declaration", "indexer_declaration", "event_declaration"):
392
+ accessors = node.child("accessors")
393
+ if mode in ("prefer", "properties_only"):
394
+ if accessors is not None and lang_version >= 7:
395
+ for accessor in accessors.children:
396
+ if accessor.type == "accessor_declaration":
397
+ _block_to_arrow(accessor)
398
+ if node.type != "event_declaration" and lang_version >= 6:
399
+ _getter_to_property_arrow(node)
400
+ elif mode == "block":
401
+ if node.type != "event_declaration":
402
+ _property_arrow_to_getter(node)
403
+ accessors = node.child("accessors")
404
+ if accessors is not None:
405
+ for accessor in accessors.children:
406
+ if accessor.type == "accessor_declaration":
407
+ keyword = accessor.child("name") or accessor.first_token()
408
+ returns = keyword is not None and keyword.text == "get"
409
+ _arrow_to_block(accessor, returns)
410
+
411
+
412
+ def _block_to_arrow(node: Node) -> None:
413
+ body = node.child("body")
414
+ if body is None or body.type != "block" or len(body.children) != 3:
415
+ return
416
+ statement = body.children[1]
417
+ if _has_comments([body.children[0], body.children[2]]) or _has_comments([statement]) or _has_verbatim(statement):
418
+ return
419
+ expression = _statement_expression(statement)
420
+ if expression is None:
421
+ return
422
+ arrow = Node(type="arrow_expression_clause", field="body")
423
+ _set_children(arrow, [token("=>"), expression])
424
+ semicolon = statement.children[-1]
425
+ index = node.children.index(body)
426
+ _set_children(node, [*node.children[:index], arrow, semicolon, *node.children[index + 1 :]])
427
+
428
+
429
+ def _statement_expression(statement: Node) -> Node | None:
430
+ kids = statement.children
431
+ if not kids or kids[-1].text != ";":
432
+ return None
433
+ if statement.type == "expression_statement" and len(kids) == 2:
434
+ return kids[0]
435
+ if statement.type == "return_statement" and len(kids) == 3 and kids[1].text != "ref":
436
+ return kids[1]
437
+ if statement.type == "throw_statement" and len(kids) == 3 and kids[1].named:
438
+ thrown = Node(type="throw_expression")
439
+ _set_children(thrown, [kids[0], kids[1]])
440
+ return thrown
441
+ return None
442
+
443
+
444
+ def _returns_value(node: Node) -> bool:
445
+ """Whether a block body for this member needs ``return``."""
446
+ if node.type in ("constructor_declaration", "destructor_declaration"):
447
+ return False
448
+ if node.type in ("operator_declaration", "conversion_operator_declaration"):
449
+ return True
450
+ returns = node.child("returns") or node.child("type")
451
+ if returns is None:
452
+ return False
453
+ text = "".join(t.text or "" for t in returns.tokens())
454
+ if text == "void":
455
+ return False
456
+ is_async = any(c.type == "modifier" and c.first_token() and c.first_token().text == "async" for c in node.children) # type: ignore[union-attr]
457
+ if is_async and text in ("Task", "ValueTask", "System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask"):
458
+ return False
459
+ return True
460
+
461
+
462
+ def _arrow_to_block(node: Node, returns: bool) -> None:
463
+ arrow = next((c for c in node.children if c.type == "arrow_expression_clause"), None)
464
+ if arrow is None or len(arrow.children) != 2:
465
+ return
466
+ index = node.children.index(arrow)
467
+ after = node.children[index + 1 :]
468
+ if not after or after[0].text != ";":
469
+ return
470
+ semicolon = after[0]
471
+ arrow_tok, expression = arrow.children
472
+ if arrow_tok.leading or arrow_tok.trailing:
473
+ return
474
+ if expression.type == "throw_expression" and len(expression.children) == 2:
475
+ statement = Node(type="throw_statement")
476
+ _set_children(statement, [expression.children[0], expression.children[1], semicolon])
477
+ elif returns:
478
+ statement = Node(type="return_statement")
479
+ _set_children(statement, [token("return"), expression, semicolon])
480
+ else:
481
+ statement = Node(type="expression_statement")
482
+ _set_children(statement, [expression, semicolon])
483
+ body = Node(type="block", field="body")
484
+ _set_children(body, [token("{"), statement, token("}")])
485
+ _set_children(node, [*node.children[:index], body, *after[1:]])
486
+
487
+
488
+ def _getter_to_property_arrow(node: Node) -> None:
489
+ accessors = node.child("accessors")
490
+ if accessors is None or node.children[-1] is not accessors or _frozen(accessors) or _has_verbatim(accessors):
491
+ return
492
+ inner = accessors.children[1:-1]
493
+ if len(inner) != 1 or inner[0].type != "accessor_declaration":
494
+ return
495
+ accessor = inner[0]
496
+ if [c.text for c in accessor.children[:1]] != ["get"]:
497
+ return
498
+ arrow = accessor.first_of("arrow_expression_clause")
499
+ if arrow is None or len(accessor.children) != 3 or accessor.children[2].text != ";":
500
+ return
501
+ if _has_comments([accessors.children[0], accessors.children[-1], accessor.children[0]]):
502
+ return
503
+ arrow.field = "value"
504
+ _set_children(node, [*node.children[:-1], arrow, accessor.children[2]])
505
+
506
+
507
+ def _property_arrow_to_getter(node: Node) -> None:
508
+ arrow = node.child("value")
509
+ if arrow is None or arrow.type != "arrow_expression_clause" or len(arrow.children) != 2:
510
+ return
511
+ index = node.children.index(arrow)
512
+ after = node.children[index + 1 :]
513
+ if [c.text for c in after] != [";"]:
514
+ return
515
+ accessor = Node(type="accessor_declaration")
516
+ get = token("get")
517
+ get.field = "name"
518
+ block_arrow = Node(type="arrow_expression_clause", field="body")
519
+ _set_children(block_arrow, arrow.children)
520
+ _set_children(accessor, [get, block_arrow, after[0]])
521
+ _arrow_to_block(accessor, True)
522
+ accessors = Node(type="accessor_list", field="accessors")
523
+ _set_children(accessors, [token("{"), accessor, token("}")])
524
+ _set_children(node, [*node.children[:index], accessors])
525
+
526
+
527
+ # Parentheses -------------------------------------------------------------
528
+
529
+
530
+ def remove_parentheses(root: Node, mode: str) -> None:
531
+ for node in list(walk(root)):
532
+ if node.type != "parenthesized_expression" or _frozen(node) or node.parent is None:
533
+ continue
534
+ kids = node.children
535
+ if len(kids) != 3 or kids[0].text != "(" or kids[2].text != ")":
536
+ continue
537
+ if kids[0].leading or kids[0].trailing or kids[2].leading or kids[2].trailing:
538
+ continue
539
+ inner = kids[1]
540
+ if mode == "remove_obvious" and not _obviously_redundant(node, inner):
541
+ continue
542
+ if mode == "remove_redundant" and not (_obviously_redundant(node, inner) or _precedence_redundant(node, inner)):
543
+ continue
544
+ if _next_token_text(node) == "<" and inner.type == "identifier":
545
+ continue
546
+ parent = node.parent
547
+ inner.field = node.field
548
+ index = parent.children.index(node)
549
+ parent.children[index] = inner
550
+ inner.parent = parent
551
+
552
+
553
+ def _obviously_redundant(node: Node, inner: Node) -> bool:
554
+ parent = node.parent
555
+ assert parent is not None
556
+ if node.field == "condition" and parent.type in ("if_statement", "while_statement", "do_statement"):
557
+ return True
558
+ if parent.type in ("return_statement", "throw_statement", "yield_statement"):
559
+ return parent.children[-1].text == ";" and parent.children[-2] is node
560
+ if inner.type == "identifier":
561
+ return True
562
+ if inner.type in _LITERALS:
563
+ numeric = inner.type in ("integer_literal", "real_literal")
564
+ return not (numeric and parent.type in ("member_access_expression", "element_access_expression", "invocation_expression", "conditional_access_expression", "postfix_unary_expression"))
565
+ if inner.type == "parenthesized_expression":
566
+ return True
567
+ return False
568
+
569
+
570
+ _PRIMARY = frozenset(
571
+ {
572
+ "identifier",
573
+ "member_access_expression",
574
+ "invocation_expression",
575
+ "element_access_expression",
576
+ "object_creation_expression",
577
+ "typeof_expression",
578
+ "default_expression",
579
+ "sizeof_expression",
580
+ "checked_expression",
581
+ "generic_name",
582
+ "this_expression",
583
+ "parenthesized_expression",
584
+ "interpolated_string_expression",
585
+ }
586
+ )
587
+
588
+ _PRECEDENCE = {
589
+ "??": 2,
590
+ "||": 3,
591
+ "&&": 4,
592
+ "|": 5,
593
+ "^": 6,
594
+ "&": 7,
595
+ "==": 8,
596
+ "!=": 8,
597
+ "<": 9,
598
+ ">": 9,
599
+ "<=": 9,
600
+ ">=": 9,
601
+ "<<": 10,
602
+ ">>": 10,
603
+ ">>>": 10,
604
+ "+": 11,
605
+ "-": 11,
606
+ "*": 12,
607
+ "/": 12,
608
+ "%": 12,
609
+ }
610
+
611
+ # Contexts where any expression may stand without parentheses.
612
+ _OPEN_CONTEXTS = frozenset(
613
+ {
614
+ "argument",
615
+ "arrow_expression_clause",
616
+ "variable_declarator",
617
+ "expression_statement",
618
+ "initializer_expression",
619
+ "return_statement",
620
+ "throw_statement",
621
+ "yield_statement",
622
+ "interpolation",
623
+ "attribute_argument",
624
+ }
625
+ )
626
+
627
+
628
+ def _precedence_redundant(node: Node, inner: Node) -> bool:
629
+ parent = node.parent
630
+ assert parent is not None
631
+ # Next to a type test or a pattern, the grammar can read the operators
632
+ # differently without the parentheses: x is T || (y is A or B).
633
+ neighbours = [inner, *(c for c in parent.children if c is not node)] if parent.type == "binary_expression" else [inner]
634
+ if any(_has_type_test(n) for n in neighbours) or _misparsed_access(inner):
635
+ return False
636
+ if parent.type in _OPEN_CONTEXTS:
637
+ return inner.type not in ("assignment_expression",) or parent.type == "expression_statement"
638
+ if parent.type == "assignment_expression" and node.field == "right":
639
+ return True
640
+ if inner.type in _PRIMARY and parent.type not in ("cast_expression",):
641
+ return inner.type not in ("integer_literal", "real_literal")
642
+ if parent.type == "binary_expression" and inner.type == "binary_expression":
643
+ outer_op = parent.child("operator")
644
+ inner_op = inner.child("operator")
645
+ if outer_op is None or inner_op is None:
646
+ return False
647
+ outer = _PRECEDENCE.get(outer_op.text or "")
648
+ own = _PRECEDENCE.get(inner_op.text or "")
649
+ if outer is None or own is None:
650
+ return False
651
+ if own > outer:
652
+ return True
653
+ if own == outer and node.field == "left" and outer_op.text != "??":
654
+ return True
655
+ return False
656
+
657
+
658
+ def _misparsed_access(node: Node) -> bool:
659
+ """A binary expression as the target of an access or call, which only a
660
+ misparse makes: the grammar reads a?[i] ?? b?.M() as (a?[i] ?? b)?.M()."""
661
+ for n in walk(node):
662
+ if n.type in ("conditional_access_expression", "member_access_expression", "invocation_expression"):
663
+ target = n.child("condition") or n.child("expression") or n.child("function")
664
+ if target is not None and target.type == "binary_expression":
665
+ return True
666
+ return False
667
+
668
+
669
+ def _has_type_test(node: Node) -> bool:
670
+ return any(
671
+ n.type in ("is_pattern_expression", "is_expression", "as_expression") or n.type.endswith("_pattern")
672
+ for n in walk(node)
673
+ )
674
+
675
+
676
+ def _next_token_text(node: Node) -> str | None:
677
+ last = node.last_token()
678
+ current: Node | None = node
679
+ while current is not None and current.parent is not None:
680
+ siblings = current.parent.children
681
+ index = siblings.index(current)
682
+ if index + 1 < len(siblings):
683
+ following = siblings[index + 1].first_token()
684
+ return following.text if following is not None else None
685
+ current = current.parent
686
+ del last
687
+ return None
688
+
689
+
690
+ # Braces ----------------------------------------------------------------
691
+
692
+
693
+ def add_braces(root: Node) -> None:
694
+ for node in list(walk(root)):
695
+ if _frozen(node):
696
+ continue
697
+ for index, child in enumerate(node.children):
698
+ if not is_embedded(node, child) or child.type == "block":
699
+ continue
700
+ if node.type == "if_statement" and child.field == "alternative" and child.type == "if_statement":
701
+ continue
702
+ if child.type in ("using_statement", "fixed_statement") and child.type == node.type:
703
+ continue
704
+ block = Node(type="block", field=child.field)
705
+ child.field = None
706
+ _set_children(block, [token("{"), child, token("}")])
707
+ node.children[index] = block
708
+ block.parent = node
709
+
710
+
711
+ # Trailing commas -----------------------------------------------------------
712
+
713
+
714
+ def remove_trailing_commas(root: Node) -> None:
715
+ for node in walk(root):
716
+ if node.type not in _LIST_WITH_TRAILING_COMMA or _frozen(node) or len(node.children) < 3:
717
+ continue
718
+ comma = node.children[-2]
719
+ close = node.children[-1]
720
+ if comma.text != "," or comma.named or close.text not in ("}", "]"):
721
+ continue
722
+ before = node.children[-3].last_token()
723
+ if before is None:
724
+ continue
725
+ before.trailing.extend(comma.trailing)
726
+ close.leading[:0] = _as_leading(comma.leading)
727
+ del node.children[-2]
728
+
729
+
730
+ def _token_text(node: Node) -> str:
731
+ first = node.first_token()
732
+ return (first.text or "") if first is not None else ""