prettycsharp 1.1.4__tar.gz → 1.1.5__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.4 → prettycsharp-1.1.5}/CHANGELOG.md +12 -0
  2. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/DESIGN.md +1 -0
  3. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/PKG-INFO +1 -1
  4. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/__init__.py +1 -1
  5. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/conditional.py +40 -2
  6. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/formatter.py +1 -0
  7. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/printer/core.py +19 -0
  8. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/printer/declarations.py +6 -1
  9. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/printer/expressions.py +15 -3
  10. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/printer/statements.py +4 -0
  11. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/settings.py +2 -0
  12. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_conditional.py +100 -2
  13. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_layout.py +36 -0
  14. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_settings.py +6 -0
  15. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.gitattributes +0 -0
  16. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/codeql/codeql-config.yml +0 -0
  17. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/dependabot.yml +0 -0
  18. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/build.in +0 -0
  19. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/build.txt +0 -0
  20. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/fuzz.in +0 -0
  21. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/fuzz.txt +0 -0
  22. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/pip-audit.in +0 -0
  23. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/pip-audit.txt +0 -0
  24. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/runtime.in +0 -0
  25. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/runtime.txt +0 -0
  26. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/test.in +0 -0
  27. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/test.txt +0 -0
  28. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/zizmor.in +0 -0
  29. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/requirements/zizmor.txt +0 -0
  30. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/accepted.toml +0 -0
  31. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/clamav/Dockerfile +0 -0
  32. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/malware-accepted.toml +0 -0
  33. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/semgrep/Dockerfile +0 -0
  34. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/yara.json +0 -0
  35. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/security/yara_update.py +0 -0
  36. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/auto-merge.yml +0 -0
  37. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/ci.yml +0 -0
  38. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/fuzz.yml +0 -0
  39. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/malware-scan.yml +0 -0
  40. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/publish.yml +0 -0
  41. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/scorecard.yml +0 -0
  42. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/security.yml +0 -0
  43. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/workflows/update-yara-rules.yml +0 -0
  44. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.github/zizmor.yml +0 -0
  45. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/.gitignore +0 -0
  46. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/AGENTS.md +0 -0
  47. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/CLAUDE.md +0 -0
  48. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/LICENSE +0 -0
  49. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/README.md +0 -0
  50. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/SECURITY.md +0 -0
  51. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/fuzz/fuzz_formatter.py +0 -0
  52. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/pyproject.toml +0 -0
  53. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/scripts/security/malware_report.py +0 -0
  54. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/scripts/security/security_report.py +0 -0
  55. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/__main__.py +0 -0
  56. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/cli.py +0 -0
  57. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/config.py +0 -0
  58. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/doc.py +0 -0
  59. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/ignore.py +0 -0
  60. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/isolate.py +0 -0
  61. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/parsing.py +0 -0
  62. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/printer/__init__.py +0 -0
  63. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/project.py +0 -0
  64. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/py.typed +0 -0
  65. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/rewrites.py +0 -0
  66. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/syntax.py +0 -0
  67. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/src/prettycsharp/verify.py +0 -0
  68. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/comment_only_accessor_list.cs +0 -0
  69. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
  70. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/directives.cs +0 -0
  71. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
  72. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
  73. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/members.cs +0 -0
  74. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/order.cs +0 -0
  75. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
  76. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
  77. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/statements.cs +0 -0
  78. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/fuzz_corpus/csharp/strings.cs +0 -0
  79. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/support.py +0 -0
  80. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_cli.py +0 -0
  81. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_config.py +0 -0
  82. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_doc.py +0 -0
  83. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_docs.py +0 -0
  84. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_format_around.py +0 -0
  85. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_fuzz_corpus.py +0 -0
  86. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_ignore.py +0 -0
  87. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_parsing.py +0 -0
  88. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_project.py +0 -0
  89. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_rewrites.py +0 -0
  90. {prettycsharp-1.1.4 → prettycsharp-1.1.5}/tests/test_verify.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  Each release's section becomes the body of its GitHub release.
4
4
 
5
+ ## [1.1.5] - 2026-10-18
6
+
7
+ - Nullable array types print tight, `object?[]?`, instead of
8
+ `object? []?`, and spread elements are spaced, `[.. items]`, as Visual
9
+ Studio formats them (IDE0055). Ranges such as `items[..2]` stay tight.
10
+ - An empty collection `[]` is never split across lines. When it does not
11
+ fit, the line breaks after `=>` or `=` and `[]` goes to the next line.
12
+ - No blank line is left directly above or below `#if`, `#elif`, `#else`
13
+ and `#endif`. A comment above an `#if` keeps the blank line above the
14
+ comment. The new setting `directive_blank_lines = "preserve"` keeps the
15
+ blank lines as written.
16
+
5
17
  ## [1.1.4] - 2026-10-17
6
18
 
7
19
  - An empty body `{ }` after a header that spans lines goes on its own line,
@@ -134,6 +134,7 @@ while (item != null);
134
134
  | `member_blank_lines` | `group_fields`: consecutive fields and auto-properties stay tight, one blank line around methods, constructors, and other members with bodies | `all`, `preserve` |
135
135
  | `blank_lines_around_blocks` | `true`: a block statement gets a blank line before and after it, except right after `{` or right before `}` | `false` |
136
136
  | `max_blank_lines` | `1`: author's blank lines between plain statements are kept, runs collapse to one | any integer, `0` |
137
+ | `directive_blank_lines` | `remove`: no blank line directly above or below `#if`, `#elif`, `#else` and `#endif` | `preserve` |
137
138
 
138
139
  No blank line ever follows `{` or precedes `}`. A comment directly above a block moves with
139
140
  it, so the blank line goes above the comment.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prettycsharp
3
- Version: 1.1.4
3
+ Version: 1.1.5
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.4"
7
+ __version__ = "1.1.5"
8
8
 
9
9
  __all__ = [
10
10
  "CSharpSyntaxError",
@@ -369,18 +369,53 @@ def covering_sets(source: str, usable: Callable[[str], bool] = parses) -> list[f
369
369
  return chosen
370
370
 
371
371
 
372
+ def tighten(text: str, hidden: dict[str, str]) -> str:
373
+ """Drop the blank lines next to conditional directive lines behind placeholders.
374
+
375
+ A placeholder whose hidden text starts with #if, #elif, #else or #endif
376
+ loses the blank lines above it; one whose hidden text ends with one loses
377
+ the blank lines below it.
378
+ """
379
+ lines = _split_lines(text)
380
+ drop: set[int] = set()
381
+ for number, line in enumerate(lines):
382
+ match = _PLACEHOLDER.match(_content(line))
383
+ if match is None or match.group(1) not in hidden:
384
+ continue
385
+ rows = hidden[match.group(1)].splitlines()
386
+ if _conditional(rows[0]):
387
+ above = number - 1
388
+ while above >= 0 and not _content(lines[above]).strip():
389
+ drop.add(above)
390
+ above -= 1
391
+ if _conditional(rows[-1]):
392
+ below = number + 1
393
+ while below < len(lines) and not _content(lines[below]).strip():
394
+ drop.add(below)
395
+ below += 1
396
+ return "".join(line for number, line in enumerate(lines) if number not in drop)
397
+
398
+
399
+ def _conditional(line: str) -> bool:
400
+ found = _directive(line)
401
+ return found is not None and found[0] in ("if", "elif", "else", "endif")
402
+
403
+
372
404
  def format_conditional(
373
405
  source: str,
374
406
  format_masked: Callable[[str], str],
375
407
  same_code: Callable[[str, str], bool],
376
408
  usable: Callable[[str], bool] = parses,
409
+ *,
410
+ tight: bool = False,
377
411
  ) -> str | None:
378
412
  """Format each symbol set's view of the file in turn.
379
413
 
380
414
  Returns None when no symbol set gives text that is ``usable`` (parses,
381
415
  by default). Raises ConditionalError when the result, seen through any
382
416
  of the symbol sets used, is not the same code as the source
383
- (``same_code`` compares two masked texts).
417
+ (``same_code`` compares two masked texts). With ``tight``, blank lines
418
+ next to conditional directives are dropped.
384
419
  """
385
420
  sets = covering_sets(source, usable)
386
421
  if not sets:
@@ -388,7 +423,10 @@ def format_conditional(
388
423
  text = source
389
424
  for symbols in sets:
390
425
  masked = mask(text, symbols)
391
- text = restore(format_masked(masked.text), masked.hidden)
426
+ formatted = format_masked(masked.text)
427
+ if tight:
428
+ formatted = tighten(formatted, masked.hidden)
429
+ text = restore(formatted, masked.hidden)
392
430
  for symbols in sets:
393
431
  if not same_code(mask(source, symbols).text, mask(text, symbols).text):
394
432
  raise BranchChanged("a branch of an #if would change in meaning")
@@ -92,6 +92,7 @@ def _format(source: str, settings: Settings, lang_version: float) -> FormatResul
92
92
  functools.partial(_format_view, settings=hiding, lang_version=lang_version, around=around, kept=kept),
93
93
  functools.partial(_same_view, around=around),
94
94
  _usable_around if around else conditional.parses,
95
+ tight=settings.directive_blank_lines == "remove",
95
96
  )
96
97
  except conditional.BranchChanged as error:
97
98
  raise FormattingError(str(error)) from None
@@ -72,6 +72,9 @@ _MERGING_PAIRS = {
72
72
  _URL = re.compile(r"\w+://")
73
73
  _LIST_ITEM = re.compile(r"^(?:[-*+]\s|\d+[.)]\s)")
74
74
 
75
+ # Directives that open a conditional branch; as list items they end in #endif.
76
+ _CONDITIONAL_TYPES = frozenset({"preproc_if", "preproc_elif", "preproc_else"})
77
+
75
78
  _INDENTING = frozenset(
76
79
  {"declaration_list", "block", "accessor_list", "enum_member_declaration_list"}
77
80
  )
@@ -344,6 +347,22 @@ class Core:
344
347
  return first.leading[0].newlines_before >= 2
345
348
  return first.newlines_before >= 2
346
349
 
350
+ def tight_directive(self, previous: Node, item: Node) -> bool:
351
+ """Whether ``directive_blank_lines`` removes the blank line between two items.
352
+
353
+ An #if item ends in #endif; a member can start with an #if around its
354
+ attributes. A comment above an #if keeps the blank line above it,
355
+ since that line is not next to the directive.
356
+ """
357
+ if self.settings.directive_blank_lines != "remove":
358
+ return False
359
+ if previous.type in _CONDITIONAL_TYPES:
360
+ return True
361
+ starts_with_if = item.type in _CONDITIONAL_TYPES or (
362
+ bool(item.children) and item.children[0].type == "preproc_if_in_attribute_list"
363
+ )
364
+ return starts_with_if and not self.has_comments_before(item)
365
+
347
366
  def has_comments_before(self, node: Node) -> bool:
348
367
  first = node.first_token()
349
368
  return first is not None and bool(first.leading)
@@ -71,6 +71,8 @@ class Declarations(Statements):
71
71
  return parts
72
72
 
73
73
  def top_blank(self, previous: Node, item: Node) -> bool:
74
+ if self.tight_directive(previous, item):
75
+ return False
74
76
  if previous.type == "file_scoped_namespace_declaration":
75
77
  return self.settings.max_blank_lines > 0
76
78
  if previous.type in DIRECTIVE_TYPES or item.type in DIRECTIVE_TYPES:
@@ -141,7 +143,8 @@ class Declarations(Statements):
141
143
  node.text is not None and node.text.startswith("#") and node.column == 0 and not node.leading
142
144
  )
143
145
  line: Doc = HARDLINE
144
- if self.has_blank_before(node):
146
+ # Every line placed here is next to a directive line.
147
+ if self.has_blank_before(node) and self.settings.directive_blank_lines != "remove":
145
148
  line = [HARDLINE, HARDLINE]
146
149
  return [line, TRIM_INDENT] if at_zero else line
147
150
 
@@ -222,6 +225,8 @@ class Declarations(Statements):
222
225
  return self.print_list(items, self.member_blank, aligned=aligned)
223
226
 
224
227
  def member_blank(self, previous: Node, item: Node) -> bool:
228
+ if self.tight_directive(previous, item):
229
+ return False
225
230
  if previous.type in DIRECTIVE_TYPES or item.type in DIRECTIVE_TYPES:
226
231
  return self.directive_blank(previous, item)
227
232
  if self.settings.max_blank_lines == 0:
@@ -277,11 +277,19 @@ class Expressions(Core):
277
277
  if value.verbatim:
278
278
  return False
279
279
  if value.type in _BLOCK_LIKE:
280
- return True
280
+ # An empty `[]` is one word: it moves to the next line whole.
281
+ return not self._empty_collection(value)
281
282
  if value.type in _CREATION_TYPES:
282
283
  return value.child("initializer") is not None or value.first_of("initializer_expression") is not None
283
284
  return False
284
285
 
286
+ def _empty_collection(self, node: Node) -> bool:
287
+ """An empty collection expression with no comments inside: `[]`."""
288
+ if node.type != "collection_expression" or len(node.children) != 2:
289
+ return False
290
+ open_tok, close_tok = node.children
291
+ return not open_tok.trailing and not close_tok.leading
292
+
285
293
  # Member chains -------------------------------------------------------
286
294
 
287
295
  def p_invocation_expression(self, node: Node) -> Doc:
@@ -655,6 +663,8 @@ class Expressions(Core):
655
663
  open_tok = node.children[0]
656
664
  close_tok = node.children[-1]
657
665
  items, commas = self.items_and_commas(node)
666
+ if self._empty_collection(node):
667
+ return [self.tok(open_tok), self.tok(close_tok)]
658
668
  if not items:
659
669
  return group([self.tok(open_tok), self.closing(close_tok, SOFTLINE)])
660
670
  docs = self._comma_docs(items, commas, may_add=True)
@@ -1005,7 +1015,8 @@ class Expressions(Core):
1005
1015
  return self.join_tokens([(c, self.print(c)) for c in node.children])
1006
1016
 
1007
1017
  def p_array_type(self, node: Node) -> Doc:
1008
- return self.join_tokens([(c, self.print(c)) for c in node.children])
1018
+ # A rank follows its element type tight, nullable or not: `object?[]`.
1019
+ return [self.print(c) for c in node.children]
1009
1020
 
1010
1021
  def p_array_rank_specifier(self, node: Node) -> Doc:
1011
1022
  parts: list[Doc] = []
@@ -1040,7 +1051,8 @@ class Expressions(Core):
1040
1051
  return self.generic(node)
1041
1052
 
1042
1053
  def p_spread_element(self, node: Node) -> Doc:
1043
- return self.join_tokens([(c, self.print(c)) for c in node.children])
1054
+ # Visual Studio spaces a spread, `[.. items]`, unlike a range, `items[..2]`.
1055
+ return self.spaced(node.children)
1044
1056
 
1045
1057
 
1046
1058
  class _OperatorMarker(Node):
@@ -76,6 +76,8 @@ class Statements(Expressions):
76
76
  return self.print_list(items, self.statement_blank, aligned=self.aligned_assignments(items))
77
77
 
78
78
  def statement_blank(self, previous: Node, item: Node) -> bool:
79
+ if self.tight_directive(previous, item):
80
+ return False
79
81
  if previous.type in DIRECTIVE_TYPES or item.type in DIRECTIVE_TYPES:
80
82
  return self.has_blank_before(item)
81
83
  if self.settings.blank_lines_around_blocks and (self.is_block_statement(previous) or self.is_block_statement(item)):
@@ -321,6 +323,8 @@ class Statements(Expressions):
321
323
  return [self.tok(open_tok), body, self.close_comments(close_tok), HARDLINE, self.tok(close_tok, leading=False)]
322
324
 
323
325
  def section_blank(self, previous: Node, item: Node) -> bool:
326
+ if self.tight_directive(previous, item):
327
+ return False
324
328
  if previous.type in DIRECTIVE_TYPES or item.type in DIRECTIVE_TYPES:
325
329
  return self.has_blank_before(item)
326
330
  if not self.settings.switch_case_blank_lines or self.settings.max_blank_lines == 0:
@@ -58,6 +58,7 @@ class Settings:
58
58
  member_blank_lines: str = "group_fields"
59
59
  blank_lines_around_blocks: bool = True
60
60
  max_blank_lines: int = 1
61
+ directive_blank_lines: str = "remove"
61
62
 
62
63
  argument_wrap: str = "one_per_line"
63
64
  chain_break_threshold: int = 2
@@ -128,6 +129,7 @@ _CHOICES: dict[str, tuple[str, ...]] = {
128
129
  "braces": ("preserve", "always", "single_line_only"),
129
130
  "empty_bodies": ("inline", "allman"),
130
131
  "member_blank_lines": ("group_fields", "all", "preserve"),
132
+ "directive_blank_lines": ("remove", "preserve"),
131
133
  "argument_wrap": ("one_per_line", "one_per_line_close_on_last", "fill", "align"),
132
134
  "expression_body_chain": ("own_line", "hang"),
133
135
  "binary_wrap": ("before_operator_paren_own_line", "before_operator", "after_operator"),
@@ -87,6 +87,106 @@ def test_covering_sets_reach_every_branch() -> None:
87
87
  assert frozenset({"B"}) in sets
88
88
 
89
89
 
90
+ def test_no_blank_lines_next_to_conditional_directives() -> None:
91
+ # The first parses whole; the second splits a class header, so each view
92
+ # is formatted on its own.
93
+ check(
94
+ """
95
+ class C
96
+ {
97
+ int a;
98
+
99
+ #if X
100
+
101
+ int b;
102
+
103
+ #else
104
+
105
+ int c;
106
+
107
+ #endif
108
+
109
+ void M() { A();
110
+
111
+ #if DEBUG
112
+ Log();
113
+ #endif
114
+
115
+ B(); }
116
+
117
+ #if X
118
+ [A]
119
+ #endif
120
+ void N() { }
121
+ }
122
+ """,
123
+ """
124
+ class C
125
+ {
126
+ int a;
127
+ #if X
128
+ int b;
129
+ #else
130
+ int c;
131
+ #endif
132
+ void M()
133
+ {
134
+ A();
135
+ #if DEBUG
136
+ Log();
137
+ #endif
138
+ B();
139
+ }
140
+ #if X
141
+ [A]
142
+ #endif
143
+ void N() { }
144
+ }
145
+ """,
146
+ )
147
+ check(
148
+ """
149
+ #if NET
150
+
151
+ class A : B
152
+ {
153
+
154
+ #else
155
+
156
+ class A
157
+ {
158
+
159
+ #endif
160
+
161
+ int a;
162
+
163
+ #if X
164
+
165
+ int b;
166
+
167
+ #endif
168
+
169
+ void M() { }
170
+ }
171
+ """,
172
+ """
173
+ #if NET
174
+ class A : B
175
+ {
176
+ #else
177
+ class A
178
+ {
179
+ #endif
180
+ int a;
181
+ #if X
182
+ int b;
183
+ #endif
184
+ void M() { }
185
+ }
186
+ """,
187
+ )
188
+
189
+
90
190
  def test_split_class_header_formats_every_branch() -> None:
91
191
  check(
92
192
  """
@@ -120,7 +220,6 @@ def test_split_class_header_formats_every_branch() -> None:
120
220
  #endif
121
221
  {
122
222
  public static int Size = 3;
123
-
124
223
  #if NET10_0_OR_GREATER
125
224
  public ValueTask DisposeAsync() => ValueTask.CompletedTask;
126
225
  #else
@@ -194,7 +293,6 @@ def test_usings_do_not_move_across_directives() -> None:
194
293
  #endif
195
294
  using Beta;
196
295
  using Gamma;
197
-
198
296
  #if NET
199
297
  class C : I
200
298
  #else
@@ -707,6 +707,42 @@ def test_obvious_parentheses_are_removed_and_clarifying_ones_kept() -> None:
707
707
  )
708
708
 
709
709
 
710
+ def test_nullable_array_types_stay_tight() -> None:
711
+ source = (
712
+ "void M(params object?[]? values) { }\n"
713
+ "\n"
714
+ "int?[] a = new int?[3];\n"
715
+ "List<string?[]> b = (string?[][])o;\n"
716
+ "var c = x is int?[] d;\n"
717
+ "var e = f ? [1] : [2];\n"
718
+ "var g = h?[i];\n"
719
+ )
720
+ assert fmt(source) == source
721
+
722
+
723
+ def test_empty_collection_moves_to_the_next_line_whole() -> None:
724
+ check(
725
+ "class C { IEnumerable<Entry> Entries(string path, IMimeTypeProvider provider) => []; int[] xs = []; }\n",
726
+ """
727
+ class C
728
+ {
729
+ IEnumerable<Entry> Entries(string path, IMimeTypeProvider provider) =>
730
+ [];
731
+
732
+ int[] xs = [];
733
+ }
734
+ """,
735
+ max_line_length=74,
736
+ )
737
+
738
+
739
+ def test_spread_elements_are_spaced_and_ranges_are_not() -> None:
740
+ assert fmt("x = [..entries, 1, ..more];\n") == "x = [.. entries, 1, .. more];\n"
741
+ assert fmt("var r = entries[1..] + entries[..2] + entries[..];\n") == (
742
+ "var r = entries[1..] + entries[..2] + entries[..];\n"
743
+ )
744
+
745
+
710
746
  def test_empty_body_after_a_header_that_spans_lines_gets_its_own_line() -> None:
711
747
  # Visual Studio (IDE0055) keeps `{ }` on the header's line only when the
712
748
  # header is one line.
@@ -315,6 +315,12 @@ def test_comment_space_and_wrap_off() -> None:
315
315
  )
316
316
 
317
317
 
318
+ def test_directive_blank_lines_preserve() -> None:
319
+ source = "class C\n{\n int a;\n\n#if X\n\n int b;\n\n#endif\n\n int c;\n}\n"
320
+ assert fmt(source, directive_blank_lines="preserve") == source
321
+ assert fmt(source) == "class C\n{\n int a;\n#if X\n int b;\n#endif\n int c;\n}\n"
322
+
323
+
318
324
  def test_max_blank_lines_zero() -> None:
319
325
  assert fmt("void M() { A();\n\n B(); }\n", max_blank_lines=0) == "void M()\n{\n A();\n B();\n}\n"
320
326
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes