prettycsharp 1.1.2__tar.gz → 1.1.4__tar.gz

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.
Files changed (90) hide show
  1. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/CHANGELOG.md +13 -0
  2. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/DESIGN.md +1 -1
  3. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/PKG-INFO +1 -1
  4. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/__init__.py +1 -1
  5. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/printer/core.py +21 -1
  6. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/printer/declarations.py +23 -14
  7. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/printer/expressions.py +4 -2
  8. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/printer/statements.py +9 -4
  9. prettycsharp-1.1.4/tests/fuzz_corpus/csharp/comment_only_accessor_list.cs +0 -0
  10. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_layout.py +104 -0
  11. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.gitattributes +0 -0
  12. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/codeql/codeql-config.yml +0 -0
  13. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/dependabot.yml +0 -0
  14. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/build.in +0 -0
  15. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/build.txt +0 -0
  16. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/fuzz.in +0 -0
  17. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/fuzz.txt +0 -0
  18. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/pip-audit.in +0 -0
  19. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/pip-audit.txt +0 -0
  20. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/runtime.in +0 -0
  21. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/runtime.txt +0 -0
  22. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/test.in +0 -0
  23. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/test.txt +0 -0
  24. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/zizmor.in +0 -0
  25. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/requirements/zizmor.txt +0 -0
  26. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/accepted.toml +0 -0
  27. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/clamav/Dockerfile +0 -0
  28. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/malware-accepted.toml +0 -0
  29. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/semgrep/Dockerfile +0 -0
  30. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/yara.json +0 -0
  31. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/security/yara_update.py +0 -0
  32. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/auto-merge.yml +0 -0
  33. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/ci.yml +0 -0
  34. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/fuzz.yml +0 -0
  35. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/malware-scan.yml +0 -0
  36. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/publish.yml +0 -0
  37. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/scorecard.yml +0 -0
  38. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/security.yml +0 -0
  39. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/workflows/update-yara-rules.yml +0 -0
  40. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.github/zizmor.yml +0 -0
  41. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/.gitignore +0 -0
  42. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/AGENTS.md +0 -0
  43. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/CLAUDE.md +0 -0
  44. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/LICENSE +0 -0
  45. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/README.md +0 -0
  46. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/SECURITY.md +0 -0
  47. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/fuzz/fuzz_formatter.py +0 -0
  48. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/pyproject.toml +0 -0
  49. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/scripts/security/malware_report.py +0 -0
  50. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/scripts/security/security_report.py +0 -0
  51. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/__main__.py +0 -0
  52. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/cli.py +0 -0
  53. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/conditional.py +0 -0
  54. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/config.py +0 -0
  55. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/doc.py +0 -0
  56. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/formatter.py +0 -0
  57. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/ignore.py +0 -0
  58. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/isolate.py +0 -0
  59. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/parsing.py +0 -0
  60. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/printer/__init__.py +0 -0
  61. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/project.py +0 -0
  62. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/py.typed +0 -0
  63. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/rewrites.py +0 -0
  64. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/settings.py +0 -0
  65. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/syntax.py +0 -0
  66. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/src/prettycsharp/verify.py +0 -0
  67. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
  68. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/directives.cs +0 -0
  69. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
  70. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
  71. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/members.cs +0 -0
  72. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/order.cs +0 -0
  73. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
  74. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
  75. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/statements.cs +0 -0
  76. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/fuzz_corpus/csharp/strings.cs +0 -0
  77. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/support.py +0 -0
  78. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_cli.py +0 -0
  79. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_conditional.py +0 -0
  80. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_config.py +0 -0
  81. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_doc.py +0 -0
  82. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_docs.py +0 -0
  83. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_format_around.py +0 -0
  84. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_fuzz_corpus.py +0 -0
  85. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_ignore.py +0 -0
  86. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_parsing.py +0 -0
  87. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_project.py +0 -0
  88. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_rewrites.py +0 -0
  89. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_settings.py +0 -0
  90. {prettycsharp-1.1.2 → prettycsharp-1.1.4}/tests/test_verify.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  Each release's section becomes the body of its GitHub release.
4
4
 
5
+ ## [1.1.4] - 2026-10-17
6
+
7
+ - An empty body `{ }` after a header that spans lines goes on its own line,
8
+ as Visual Studio formats it. Before, `) { }` after a broken parameter
9
+ list, constructor initializer, `where` clause, base list, catch filter or
10
+ lambda parameter list was flagged by IDE0055. A header on one line keeps
11
+ `{ }` beside it.
12
+
13
+ ## [1.1.3] - 2026-10-17
14
+
15
+ - A property's accessor list or an enum that holds only a comment no longer
16
+ gains a blank line each time the file is formatted. Found by fuzzing.
17
+
5
18
  ## [1.1.2] - 2026-10-17
6
19
 
7
20
  - Nothing is padded into columns by default any more: `align_assignments`,
@@ -104,7 +104,7 @@ stops the search, and the file nearest the source wins.
104
104
  | `brace_style` | `allman` | `kr`, `mixed` (Allman for types and members, K&R in method bodies) |
105
105
  | `continuation_keywords` | `blank_line`: `else`, `else if`, `catch`, `finally`, and a `do` loop's `while` go on their own line after a blank line | `new_line`, `same_line` |
106
106
  | `braces` | `preserve` | `always`, `single_line_only` |
107
- | `empty_bodies` | `inline`: `{ }` on the declaration line | `allman` |
107
+ | `empty_bodies` | `inline`: `{ }` on the declaration line, or on its own line when the header spans lines | `allman` |
108
108
 
109
109
  Short auto-property accessors always stay on one line: `public int Id { get; set; }`.
110
110
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prettycsharp
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: An opinionated formatter for C#, in the spirit of Prettier
5
5
  Project-URL: Homepage, https://github.com/WilliamSmithEdward/prettyCSharp
6
6
  Project-URL: Repository, https://github.com/WilliamSmithEdward/prettyCSharp
@@ -4,7 +4,7 @@ from prettycsharp.formatter import FormatResult, FormattingError, format_details
4
4
  from prettycsharp.parsing import CSharpSyntaxError
5
5
  from prettycsharp.settings import Settings, SettingsError
6
6
 
7
- __version__ = "1.1.2"
7
+ __version__ = "1.1.4"
8
8
 
9
9
  __all__ = [
10
10
  "CSharpSyntaxError",
@@ -22,6 +22,7 @@ from prettycsharp.doc import (
22
22
  TRIM_INDENT,
23
23
  Doc,
24
24
  Group,
25
+ IfBreak,
25
26
  LineSuffix,
26
27
  Wrapped,
27
28
  group,
@@ -90,6 +91,8 @@ class Core:
90
91
  self.file = file
91
92
  self.lang = lang_version
92
93
  self._blank_stack: list[Callable[[Node, Node], bool]] = []
94
+ # Tokens whose leading comments were printed ahead of them.
95
+ self._hoisted: set[int] = set()
93
96
  self._handlers: dict[str, Handler] = {}
94
97
  for name in dir(self):
95
98
  if name.startswith("p_"):
@@ -115,7 +118,7 @@ class Core:
115
118
  if t.text is None:
116
119
  return self.print(t)
117
120
  parts: list[Doc] = []
118
- if leading and t.leading:
121
+ if leading and t.leading and id(t) not in self._hoisted:
119
122
  parts.append(self.leading_comments(t))
120
123
  parts.append(self.token_text(t))
121
124
  if trailing and t.trailing:
@@ -133,6 +136,16 @@ class Core:
133
136
  # breaks kept as written, its continuation lines not reindented.
134
137
  return [text, BREAK_PARENT]
135
138
 
139
+ def hoist_leading(self, node: Node | None) -> Doc:
140
+ """The comments before ``node``'s first token, printed now instead of with it."""
141
+ if node is None or node.verbatim:
142
+ return ""
143
+ first = next(node.tokens(), None)
144
+ if first is None or not first.leading or first.verbatim:
145
+ return ""
146
+ self._hoisted.add(id(first))
147
+ return self.leading_comments(first)
148
+
136
149
  def leading_comments(self, t: Node, *, first_blank_allowed: bool = False) -> Doc:
137
150
  """The comments before a token, each on its own line, then the token's line."""
138
151
  # Each comment stood on its own line: start one if code is already on this line.
@@ -486,6 +499,13 @@ class Core:
486
499
  before.append(HARDLINE)
487
500
  return indent([*before, parts])
488
501
 
502
+ def after_header(self, head_id: object | None) -> Doc:
503
+ """What goes before an empty inline body: a space, or a line break when
504
+ the header group ``head_id`` spans lines, as Visual Studio formats it."""
505
+ if head_id is None:
506
+ return " "
507
+ return IfBreak(HARDLINE, " ", group_id=head_id)
508
+
489
509
  def open_brace(self, context: str) -> Doc:
490
510
  """What goes before an opening brace: a line break or a space."""
491
511
  style = self.settings.brace_style
@@ -193,7 +193,7 @@ class Declarations(Statements):
193
193
  def p_file_scoped_namespace_declaration(self, node: Node) -> Doc:
194
194
  return self.words(node.children)
195
195
 
196
- def declaration_body(self, body: Node, context: str) -> Doc:
196
+ def declaration_body(self, body: Node, context: str, head_id: object | None = None) -> Doc:
197
197
  if len(body.children) < 2 or body.verbatim:
198
198
  return [" ", self.print(body)]
199
199
  open_tok = body.children[0]
@@ -201,7 +201,7 @@ class Declarations(Statements):
201
201
  items = body.children[1:-1]
202
202
  if not items and not close_tok.leading and not open_tok.trailing:
203
203
  if self.settings.empty_bodies == "inline":
204
- return [" ", self.tok(open_tok), " ", self.tok(close_tok)]
204
+ return [self.after_header(head_id), self.tok(open_tok), " ", self.tok(close_tok)]
205
205
  return [self.open_brace(context), self.tok(open_tok), HARDLINE, self.tok(close_tok)]
206
206
  inner: Doc = ""
207
207
  if items:
@@ -261,14 +261,17 @@ class Declarations(Statements):
261
261
  if body is None:
262
262
  body = node.first_of("declaration_list", "enum_member_declaration_list")
263
263
  attributes, header, constraints, tail = self.split_declaration(node, body)
264
- parts: list[Doc] = [self.attribute_lines(attributes), self.header(header)]
264
+ comments = self.hoist_leading(header[0] if header else None)
265
+ head: list[Doc] = [self.header(header)]
265
266
  if constraints:
266
- parts.append(self.constraints(constraints))
267
+ head.append(self.constraints(constraints))
268
+ head_id = object()
269
+ parts: list[Doc] = [self.attribute_lines(attributes), comments, group(head, id=head_id)]
267
270
  if body is not None:
268
271
  if body.type == "enum_member_declaration_list":
269
- parts.append(self.enum_body(body))
272
+ parts.append(self.enum_body(body, head_id))
270
273
  else:
271
- parts.append(self.declaration_body(body, "type"))
274
+ parts.append(self.declaration_body(body, "type", head_id))
272
275
  parts.append(self.words(tail) if tail else "")
273
276
  return parts
274
277
 
@@ -363,7 +366,7 @@ class Declarations(Statements):
363
366
 
364
367
  # Enums ----------------------------------------------------------------
365
368
 
366
- def enum_body(self, body: Node) -> Doc:
369
+ def enum_body(self, body: Node, head_id: object | None = None) -> Doc:
367
370
  if len(body.children) < 2 or any(c.type in DIRECTIVE_TYPES for c in body.children):
368
371
  return [" ", self.verbatim(body)]
369
372
  open_tok = body.children[0]
@@ -371,7 +374,7 @@ class Declarations(Statements):
371
374
  items, commas = self.items_and_commas(body)
372
375
  if not items and not close_tok.leading:
373
376
  if self.settings.empty_bodies == "inline":
374
- return [" ", self.tok(open_tok), " ", self.tok(close_tok)]
377
+ return [self.after_header(head_id), self.tok(open_tok), " ", self.tok(close_tok)]
375
378
  return [self.open_brace("type"), self.tok(open_tok), HARDLINE, self.tok(close_tok)]
376
379
  members = [item[0] for item in items if len(item) == 1]
377
380
  if len(members) != len(items):
@@ -399,7 +402,7 @@ class Declarations(Statements):
399
402
  return [
400
403
  self.open_brace("type"),
401
404
  self.tok(open_tok),
402
- indent([HARDLINE, lines]),
405
+ indent([HARDLINE, lines]) if lines else "",
403
406
  self.close_comments(close_tok),
404
407
  HARDLINE,
405
408
  self.tok(close_tok, leading=False),
@@ -452,17 +455,20 @@ class Declarations(Statements):
452
455
  break
453
456
  else:
454
457
  header.append(child)
455
- parts: list[Doc] = [self.attribute_lines(attributes), self.header(header)]
458
+ comments = self.hoist_leading(header[0] if header else None)
459
+ head: list[Doc] = [self.header(header)]
456
460
  if ctor_init is not None:
457
- parts.append(group(indent([LINE, self.print(ctor_init)])))
461
+ head.append(group(indent([LINE, self.print(ctor_init)])))
458
462
  if constraints:
459
- parts.append(self.constraints(constraints))
463
+ head.append(self.constraints(constraints))
464
+ head_id = object()
465
+ parts: list[Doc] = [self.attribute_lines(attributes), comments, group(head, id=head_id)]
460
466
  rest = node.children[tail_start:]
461
467
  i = 0
462
468
  while i < len(rest):
463
469
  child = rest[i]
464
470
  if child is body and child.type == "block":
465
- parts.append(self.block(child, "member"))
471
+ parts.append(self.block(child, "member", head_id))
466
472
  elif child.type == "arrow_expression_clause":
467
473
  parts.append(self.expression_body(child))
468
474
  elif child is accessors:
@@ -522,10 +528,13 @@ class Declarations(Statements):
522
528
  return [" ", self.tok(open_tok), " ", join(" ", [self.print(a) for a in accessors]), " ", self.tok(close_tok)]
523
529
  if not accessors and not close_tok.leading:
524
530
  return [" ", self.tok(open_tok), " ", self.tok(close_tok)]
531
+ body: Doc = ""
532
+ if accessors:
533
+ body = indent([HARDLINE, self.print_list(accessors, lambda a, b: self.has_blank_before(b))])
525
534
  return [
526
535
  self.open_brace("accessor"),
527
536
  self.tok(open_tok),
528
- indent([HARDLINE, self.print_list(accessors, lambda a, b: self.has_blank_before(b))]),
537
+ body,
529
538
  self.close_comments(close_tok),
530
539
  HARDLINE,
531
540
  self.tok(close_tok, leading=False),
@@ -128,7 +128,7 @@ class _Link:
128
128
 
129
129
 
130
130
  class Expressions(Core):
131
- def block(self, block: Node, context: str) -> Doc: # pragma: no cover - Statements defines it
131
+ def block(self, block: Node, context: str, head_id: object | None = None) -> Doc: # pragma: no cover - Statements defines it
132
132
  raise NotImplementedError
133
133
 
134
134
  # Binary operators ---------------------------------------------------
@@ -724,6 +724,7 @@ class Expressions(Core):
724
724
  body = node.child("body")
725
725
  if arrow is None or body is None:
726
726
  return self.generic(node)
727
+ comments = self.hoist_leading(node.children[0]) if body.type == "block" else ""
727
728
  head: list[Doc] = []
728
729
  previous: Node | None = None
729
730
  for child in node.children:
@@ -734,7 +735,8 @@ class Expressions(Core):
734
735
  head.append(self.print(child))
735
736
  previous = child
736
737
  if body.type == "block":
737
- return [head, " ", self.tok(arrow), self.block(body, "lambda")]
738
+ head_id = object()
739
+ return [comments, group([head, " ", self.tok(arrow)], id=head_id), self.block(body, "lambda", head_id)]
738
740
  body_doc = self.print(body)
739
741
  if self.self_breaking(body) or self.block_like(body):
740
742
  return [head, " ", self.tok(arrow), " ", body_doc]
@@ -39,10 +39,13 @@ _EMBEDDED_PARENTS = frozenset(
39
39
  class Statements(Expressions):
40
40
  # Blocks ---------------------------------------------------------------
41
41
 
42
- def block(self, block: Node, context: str) -> Doc:
43
- """A block after a header: the brace's line break or space, then the braces."""
42
+ def block(self, block: Node, context: str, head_id: object | None = None) -> Doc:
43
+ """A block after a header: the brace's line break or space, then the braces.
44
+
45
+ ``head_id`` names the header's group, for headers whose empty body
46
+ moves to its own line when the header spans lines."""
44
47
  if self.is_empty_block(block) and self.settings.empty_bodies == "inline":
45
- return [" ", self.block_braces(block)]
48
+ return [self.after_header(head_id), self.block_braces(block)]
46
49
  return [self.open_brace(context), self.block_braces(block)]
47
50
 
48
51
  def is_empty_block(self, block: Node) -> bool:
@@ -365,7 +368,9 @@ class Statements(Expressions):
365
368
  block = node.children[-1] if node.children else None
366
369
  if block is None or block.type != "block":
367
370
  return self.generic(node)
368
- return [self.spaced(node.children[:-1]), self.block(block, "statement")]
371
+ comments = self.hoist_leading(node.children[0])
372
+ head_id = object()
373
+ return [comments, group(self.spaced(node.children[:-1]), id=head_id), self.block(block, "statement", head_id)]
369
374
 
370
375
  def p_finally_clause(self, node: Node) -> Doc:
371
376
  return self.p_catch_clause(node)
@@ -707,6 +707,110 @@ def test_obvious_parentheses_are_removed_and_clarifying_ones_kept() -> None:
707
707
  )
708
708
 
709
709
 
710
+ def test_empty_body_after_a_header_that_spans_lines_gets_its_own_line() -> None:
711
+ # Visual Studio (IDE0055) keeps `{ }` on the header's line only when the
712
+ # header is one line.
713
+ check(
714
+ """
715
+ // Redirects.
716
+ class RedirectModule : ModuleBase
717
+ {
718
+ public RedirectModule(string baseRoute, Uri redirectUrl, Func<IHttpContext, bool>? shouldRedirect, HttpStatusCode statusCode) { }
719
+ public RedirectModule(long alphabetical, long betamaxfactor, long gammaradiation, long deltaforce) : base(alphabetical) { }
720
+ public void Generic<TFirstTypeParameter, TSecondTypeParameter>(TFirstTypeParameter first, TSecondTypeParameter s) where TFirstTypeParameter : class { }
721
+ public RedirectModule(int a) : base() { }
722
+ // Short.
723
+ public void G<T>() where T : class { }
724
+ }
725
+ public abstract class EmptyAbstractWithAVeryLongNameIndeed<TFirstTypeParameter, TSecondTypeParameter> : Base where TFirstTypeParameter : class { }
726
+ """,
727
+ """
728
+ // Redirects.
729
+ class RedirectModule : ModuleBase
730
+ {
731
+ public RedirectModule(
732
+ string baseRoute,
733
+ Uri redirectUrl,
734
+ Func<IHttpContext, bool>? shouldRedirect,
735
+ HttpStatusCode statusCode
736
+ )
737
+ { }
738
+
739
+ public RedirectModule(long alphabetical, long betamaxfactor, long gammaradiation, long deltaforce)
740
+ : base(alphabetical)
741
+ { }
742
+
743
+ public void Generic<TFirstTypeParameter, TSecondTypeParameter>(TFirstTypeParameter first, TSecondTypeParameter s)
744
+ where TFirstTypeParameter : class
745
+ { }
746
+
747
+ public RedirectModule(int a) : base() { }
748
+
749
+ // Short.
750
+ public void G<T>() where T : class { }
751
+ }
752
+
753
+ public abstract class EmptyAbstractWithAVeryLongNameIndeed<TFirstTypeParameter, TSecondTypeParameter> : Base
754
+ where TFirstTypeParameter : class
755
+ { }
756
+ """,
757
+ )
758
+
759
+
760
+ def test_empty_catch_and_lambda_bodies_after_a_header_that_spans_lines() -> None:
761
+ check(
762
+ """
763
+ void M()
764
+ {
765
+ try { }
766
+ // Ignored.
767
+ catch (Exception error) when (IsRecoverable(error)) { }
768
+ Run((first, second, third) => { });
769
+ }
770
+ """,
771
+ """
772
+ void M()
773
+ {
774
+ try { }
775
+
776
+ // Ignored.
777
+ catch (Exception error) when (IsRecoverable(
778
+ error
779
+ ))
780
+ { }
781
+
782
+ Run((
783
+ first,
784
+ second,
785
+ third
786
+ ) =>
787
+ { });
788
+ }
789
+ """,
790
+ max_line_length=30,
791
+ )
792
+
793
+
794
+ def test_accessor_list_and_enum_holding_only_a_comment() -> None:
795
+ check(
796
+ "class C { int P {\n// c\n} }\nenum E {\n// c\n}\n",
797
+ """
798
+ class C
799
+ {
800
+ int P
801
+ {
802
+ // c
803
+ }
804
+ }
805
+
806
+ enum E
807
+ {
808
+ // c
809
+ }
810
+ """,
811
+ )
812
+
813
+
710
814
  def test_auto_properties_and_empty_bodies_stay_on_one_line() -> None:
711
815
  check(
712
816
  """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes