prettycsharp 1.1.0__tar.gz → 1.1.2__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 (89) hide show
  1. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/CHANGELOG.md +21 -0
  2. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/DESIGN.md +18 -9
  3. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/PKG-INFO +6 -5
  4. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/README.md +5 -4
  5. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/__init__.py +1 -1
  6. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/conditional.py +30 -18
  7. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/formatter.py +80 -11
  8. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/isolate.py +8 -4
  9. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/rewrites.py +3 -0
  10. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/settings.py +3 -3
  11. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_conditional.py +44 -0
  12. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_format_around.py +39 -0
  13. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_layout.py +34 -6
  14. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_rewrites.py +0 -1
  15. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_settings.py +8 -6
  16. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.gitattributes +0 -0
  17. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/codeql/codeql-config.yml +0 -0
  18. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/dependabot.yml +0 -0
  19. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/build.in +0 -0
  20. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/build.txt +0 -0
  21. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/fuzz.in +0 -0
  22. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/fuzz.txt +0 -0
  23. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/pip-audit.in +0 -0
  24. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/pip-audit.txt +0 -0
  25. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/runtime.in +0 -0
  26. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/runtime.txt +0 -0
  27. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/test.in +0 -0
  28. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/test.txt +0 -0
  29. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/zizmor.in +0 -0
  30. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/zizmor.txt +0 -0
  31. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/accepted.toml +0 -0
  32. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/clamav/Dockerfile +0 -0
  33. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/malware-accepted.toml +0 -0
  34. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/semgrep/Dockerfile +0 -0
  35. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/yara.json +0 -0
  36. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/yara_update.py +0 -0
  37. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/auto-merge.yml +0 -0
  38. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/ci.yml +0 -0
  39. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/fuzz.yml +0 -0
  40. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/malware-scan.yml +0 -0
  41. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/publish.yml +0 -0
  42. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/scorecard.yml +0 -0
  43. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/security.yml +0 -0
  44. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/update-yara-rules.yml +0 -0
  45. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/zizmor.yml +0 -0
  46. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.gitignore +0 -0
  47. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/AGENTS.md +0 -0
  48. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/CLAUDE.md +0 -0
  49. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/LICENSE +0 -0
  50. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/SECURITY.md +0 -0
  51. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/fuzz/fuzz_formatter.py +0 -0
  52. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/pyproject.toml +0 -0
  53. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/scripts/security/malware_report.py +0 -0
  54. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/scripts/security/security_report.py +0 -0
  55. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/__main__.py +0 -0
  56. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/cli.py +0 -0
  57. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/config.py +0 -0
  58. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/doc.py +0 -0
  59. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/ignore.py +0 -0
  60. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/parsing.py +0 -0
  61. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/__init__.py +0 -0
  62. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/core.py +0 -0
  63. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/declarations.py +0 -0
  64. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/expressions.py +0 -0
  65. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/statements.py +0 -0
  66. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/project.py +0 -0
  67. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/py.typed +0 -0
  68. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/syntax.py +0 -0
  69. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/verify.py +0 -0
  70. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
  71. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/directives.cs +0 -0
  72. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
  73. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
  74. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/members.cs +0 -0
  75. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/order.cs +0 -0
  76. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
  77. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
  78. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/statements.cs +0 -0
  79. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/strings.cs +0 -0
  80. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/support.py +0 -0
  81. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_cli.py +0 -0
  82. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_config.py +0 -0
  83. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_doc.py +0 -0
  84. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_docs.py +0 -0
  85. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_fuzz_corpus.py +0 -0
  86. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_ignore.py +0 -0
  87. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_parsing.py +0 -0
  88. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_project.py +0 -0
  89. {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_verify.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  Each release's section becomes the body of its GitHub release.
4
4
 
5
+ ## [1.1.2] - 2026-10-17
6
+
7
+ - Nothing is padded into columns by default any more: `align_assignments`,
8
+ `align_enum_values` and `align_switch_arms` now default to `false`, so each
9
+ `=` and `=>` has one space before it. Padded columns shift whenever one
10
+ line changes, and the diff then touches every line of the run. Set any of
11
+ the three to `true` to align as before.
12
+
13
+ ## [1.1.1] - 2026-10-17
14
+
15
+ - `syntax_errors = "format_around"` now works inside each view of a file
16
+ split by `#if`: a view that does not parse is formatted around its own
17
+ unreadable lines, so both branches of a split class header are formatted
18
+ even when a method holds syntax newer than the parser. Before, the
19
+ fallback ran on the whole file and kept the branch lines as written.
20
+ - A comma followed only by hidden `#if` lines before the closing brace is no
21
+ longer removed as a trailing comma, since in another view it separates
22
+ items. Files like this were skipped before; on ten open-source
23
+ repositories the default settings now skip 11 files instead of 14, and
24
+ `format_around` skips 2.
25
+
5
26
  ## [1.1.0] - 2026-10-17
6
27
 
7
28
  - Files whose `#if` branches split a declaration or statement, such as a
@@ -233,12 +233,14 @@ int[] codes = [200, 404];
233
233
 
234
234
  | Setting | Default | Alternatives |
235
235
  | --- | --- | --- |
236
- | `align_switch_arms` | `true` | `false` |
237
- | `align_assignments` | `true`: consecutive local assignments and field initializers | `false` |
238
- | `align_enum_values` | `true` | `false` |
236
+ | `align_switch_arms` | `false`: one space before each arm's `=>` | `true` |
237
+ | `align_assignments` | `false`: one space before each `=` | `true`: consecutive local assignments and field initializers |
238
+ | `align_enum_values` | `false`: one space before each `=` | `true` |
239
239
 
240
- Only the `=` or `=>` aligns. Types and names are not put into columns. A run ends at a blank
241
- line or at any statement of a different kind.
240
+ By default nothing is padded into columns: alignment shifts whenever one line
241
+ changes, and the diff touches every line of the run. Turned on, only the `=`
242
+ or `=>` aligns; types and names are not put into columns, and a run ends at a
243
+ blank line or at any statement of a different kind:
242
244
 
243
245
  ```csharp
244
246
  var id = order.Id;
@@ -344,8 +346,14 @@ file is formatted one view at a time (`conditional.py`):
344
346
  untouched.
345
347
 
346
348
  In this mode no rewrite moves code across a directive: the namespace style
347
- is left as it is, a using never moves past a directive, and modifiers split
348
- by one keep their order.
349
+ is left as it is, a using never moves past a directive, modifiers split by
350
+ one keep their order, and a comma with hidden lines between it and the
351
+ closing brace is not taken for a trailing comma.
352
+
353
+ With `syntax_errors = "format_around"` (below), a view that does not parse
354
+ is still used when hiding its fewest unreadable lines makes it parse: the
355
+ view is formatted around those lines, and the final check hides the same
356
+ lines, found by their text, in the view of the source and of the result.
349
357
 
350
358
  ## Code the parser cannot read
351
359
 
@@ -370,5 +378,6 @@ format; otherwise the file is skipped as before.
370
378
  ## Limits
371
379
 
372
380
  A file still is not formatted when no view of its `#if` branches parses and,
373
- with `format_around`, hiding lines does not make it parse either, as when an
374
- `#if` stands inside an expression.
381
+ with `format_around`, hiding lines makes neither a view nor the whole file
382
+ parse. Lines are hidden whole, so a one-line member that holds unreadable
383
+ syntax is kept as written in full.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prettycsharp
3
- Version: 1.1.0
3
+ Version: 1.1.2
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
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
47
47
  An opinionated formatter for C#, written in Python, in the spirit of Prettier.
48
48
  It parses each file with [tree-sitter](https://tree-sitter.github.io/) and
49
49
  reprints the whole file in one consistent layout: braces, indentation, line
50
- breaks, blank lines, alignment and a few safe rewrites such as file-scoped
50
+ breaks, blank lines and a few safe rewrites such as file-scoped
51
51
  namespaces, sorted usings and expression-bodied members. It needs no .NET
52
52
  SDK.
53
53
 
@@ -175,7 +175,7 @@ Settings come from, highest precedence first:
175
175
  max_line_length = 100
176
176
  brace_style = "kr"
177
177
  continuation_keywords = "same_line"
178
- align_assignments = false
178
+ align_assignments = true
179
179
  ```
180
180
 
181
181
  Every setting, its default, its alternatives and an example of each layout
@@ -236,8 +236,9 @@ and names the lines it kept:
236
236
  prettycsharp format src/ -o syntax_errors=format_around
237
237
  ```
238
238
 
239
- A file is still left untouched when hiding lines does not make it parse, as
240
- when an `#if` stands inside an expression.
239
+ This works together with conditional compilation: in a file split by `#if`,
240
+ each view is formatted around its own unreadable lines. A file is still left
241
+ untouched when hiding lines does not make it parse.
241
242
 
242
243
  ## License
243
244
 
@@ -12,7 +12,7 @@
12
12
  An opinionated formatter for C#, written in Python, in the spirit of Prettier.
13
13
  It parses each file with [tree-sitter](https://tree-sitter.github.io/) and
14
14
  reprints the whole file in one consistent layout: braces, indentation, line
15
- breaks, blank lines, alignment and a few safe rewrites such as file-scoped
15
+ breaks, blank lines and a few safe rewrites such as file-scoped
16
16
  namespaces, sorted usings and expression-bodied members. It needs no .NET
17
17
  SDK.
18
18
 
@@ -140,7 +140,7 @@ Settings come from, highest precedence first:
140
140
  max_line_length = 100
141
141
  brace_style = "kr"
142
142
  continuation_keywords = "same_line"
143
- align_assignments = false
143
+ align_assignments = true
144
144
  ```
145
145
 
146
146
  Every setting, its default, its alternatives and an example of each layout
@@ -201,8 +201,9 @@ and names the lines it kept:
201
201
  prettycsharp format src/ -o syntax_errors=format_around
202
202
  ```
203
203
 
204
- A file is still left untouched when hiding lines does not make it parse, as
205
- when an `#if` stands inside an expression.
204
+ This works together with conditional compilation: in a file split by `#if`,
205
+ each view is formatted around its own unreadable lines. A file is still left
206
+ untouched when hiding lines does not make it parse.
206
207
 
207
208
  ## License
208
209
 
@@ -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.0"
7
+ __version__ = "1.1.2"
8
8
 
9
9
  __all__ = [
10
10
  "CSharpSyntaxError",
@@ -219,11 +219,13 @@ def mask(source: str, defined: Iterable[str]) -> Masked:
219
219
  return masked
220
220
 
221
221
 
222
- def hide(lines: Sequence[str], keep: Sequence[bool], *, reindent: bool = False) -> Masked:
222
+ def hide(lines: Sequence[str], keep: Sequence[bool], *, reindent: bool = False, salt: str = "") -> Masked:
223
223
  """Replace each run of lines not kept by one placeholder comment line.
224
224
 
225
- ``lines`` keep their line breaks. With ``reindent``, ``restore`` moves the
226
- hidden lines to the indentation their placeholder was printed at.
225
+ ``lines`` keep their line breaks. ``salt`` keeps the ids of a second
226
+ hiding, inside text already hidden once, apart from the first. With
227
+ ``reindent``, ``restore`` moves the hidden lines to the indentation their
228
+ placeholder was printed at.
227
229
  """
228
230
  out: list[str] = []
229
231
  hidden: dict[str, str] = {}
@@ -234,7 +236,7 @@ def hide(lines: Sequence[str], keep: Sequence[bool], *, reindent: bool = False)
234
236
  return
235
237
  text = "".join(run)
236
238
  # The position makes two runs with the same text distinct.
237
- identifier = hashlib.sha256(f"{len(hidden)}:{text}".encode()).hexdigest()[:16]
239
+ identifier = hashlib.sha256(f"{salt}{len(hidden)}:{text}".encode()).hexdigest()[:16]
238
240
  hidden[identifier] = text.rstrip("\r\n")
239
241
  out.append(f"//#prettycsharp-hidden {identifier}{line_break}")
240
242
  run.clear()
@@ -254,19 +256,20 @@ def _line_break(line: str) -> str:
254
256
  return line[len(content) :] or "\n"
255
257
 
256
258
 
257
- def restore(text: str, hidden: dict[str, str], *, reindent: bool = False) -> str:
259
+ def restore(text: str, hidden: dict[str, str], *, reindent: bool = False, nested: bool = False) -> str:
258
260
  """Put back the text each placeholder line stands for.
259
261
 
260
262
  With ``reindent``, hidden lines move to the indentation their placeholder
261
263
  was printed at, unless they could hold a multi-line string, whose
262
- content must not change.
264
+ content must not change. With ``nested``, placeholders of an outer
265
+ hiding are left for its own restore.
263
266
  """
264
267
  seen: set[str] = set()
265
268
  out: list[str] = []
266
269
  for line in _split_lines(text):
267
270
  content = _content(line)
268
271
  match = _PLACEHOLDER.match(content)
269
- if match is None:
272
+ if match is None or (nested and match.group(1) not in hidden):
270
273
  out.append(line)
271
274
  continue
272
275
  identifier = match.group(1)
@@ -337,8 +340,16 @@ def branches(source: str) -> set[int]:
337
340
  return found
338
341
 
339
342
 
340
- def covering_sets(source: str) -> list[frozenset[str]]:
341
- """Symbol sets whose masked text parses and that together reach every branch they can."""
343
+ def parses(text: str) -> bool:
344
+ try:
345
+ parse(text)
346
+ except CSharpSyntaxError:
347
+ return False
348
+ return True
349
+
350
+
351
+ def covering_sets(source: str, usable: Callable[[str], bool] = parses) -> list[frozenset[str]]:
352
+ """Symbol sets whose masked text is ``usable`` and that together reach every branch they can."""
342
353
  wanted = branches(source)
343
354
  chosen: list[frozenset[str]] = []
344
355
  covered: set[int] = set()
@@ -349,9 +360,7 @@ def covering_sets(source: str) -> list[frozenset[str]]:
349
360
  new = masked.active_branches - covered
350
361
  if chosen and not new:
351
362
  continue
352
- try:
353
- parse(masked.text)
354
- except CSharpSyntaxError:
363
+ if not usable(masked.text):
355
364
  continue
356
365
  chosen.append(candidate)
357
366
  covered |= masked.active_branches
@@ -361,16 +370,19 @@ def covering_sets(source: str) -> list[frozenset[str]]:
361
370
 
362
371
 
363
372
  def format_conditional(
364
- source: str, format_masked: Callable[[str], str], same_code: Callable[[str, str], bool]
373
+ source: str,
374
+ format_masked: Callable[[str], str],
375
+ same_code: Callable[[str, str], bool],
376
+ usable: Callable[[str], bool] = parses,
365
377
  ) -> str | None:
366
378
  """Format each symbol set's view of the file in turn.
367
379
 
368
- Returns None when no symbol set gives text that parses. Raises
369
- ConditionalError when the result, seen through any of the symbol sets
370
- used, is not the same code as the source (``same_code`` compares two
371
- masked texts).
380
+ Returns None when no symbol set gives text that is ``usable`` (parses,
381
+ by default). Raises ConditionalError when the result, seen through any
382
+ of the symbol sets used, is not the same code as the source
383
+ (``same_code`` compares two masked texts).
372
384
  """
373
- sets = covering_sets(source)
385
+ sets = covering_sets(source, usable)
374
386
  if not sets:
375
387
  return None
376
388
  text = source
@@ -10,9 +10,10 @@ crashing the interpreter.
10
10
  from __future__ import annotations
11
11
 
12
12
  import dataclasses
13
+ import functools
13
14
  import sys
14
15
  import threading
15
- from collections.abc import Callable
16
+ from collections.abc import Callable, Iterable
16
17
  from typing import TypeVar
17
18
 
18
19
  from prettycsharp import conditional
@@ -80,25 +81,59 @@ def _format(source: str, settings: Settings, lang_version: float) -> FormatResul
80
81
  syntax_error = error
81
82
  # Neither rewrite may move a namespace's braces across hidden lines.
82
83
  hiding = dataclasses.replace(settings, namespace_style="preserve")
84
+ around = settings.syntax_errors == "format_around"
83
85
  if conditional.has_conditionals(source):
84
- # The #if branches together are not valid C#: format one view at a time.
86
+ # The #if branches together are not valid C#: format one view at a time,
87
+ # and with format_around, within a view, around the lines it cannot read.
88
+ kept: list[str] = []
85
89
  try:
86
90
  result = conditional.format_conditional(
87
- source, lambda text: _format_parsed(text, hiding, lang_version), _same_view
91
+ source,
92
+ functools.partial(_format_view, settings=hiding, lang_version=lang_version, around=around, kept=kept),
93
+ functools.partial(_same_view, around=around),
94
+ _usable_around if around else conditional.parses,
88
95
  )
89
96
  except conditional.BranchChanged as error:
90
97
  raise FormattingError(str(error)) from None
91
98
  except conditional.ConditionalError:
92
99
  raise syntax_error from None
93
100
  if result is not None:
94
- return FormatResult(result)
95
- if settings.syntax_errors == "format_around":
96
- around = _format_around(source, hiding, lang_version)
97
- if around is not None:
98
- return around
101
+ return FormatResult(result, _source_rows(source, kept))
102
+ if around:
103
+ whole = _format_around(source, hiding, lang_version)
104
+ if whole is not None:
105
+ return whole
99
106
  raise syntax_error
100
107
 
101
108
 
109
+ def _format_view(text: str, *, settings: Settings, lang_version: float, around: bool, kept: list[str]) -> str:
110
+ """Format one view of an #if-split file, around unreadable lines if allowed."""
111
+ try:
112
+ return _format_parsed(text, settings, lang_version)
113
+ except CSharpSyntaxError:
114
+ if not around:
115
+ raise
116
+ masked = isolate(text)
117
+ if masked is None:
118
+ raise CSharpSyntaxError(1, 1)
119
+ formatted = _format_parsed(masked.text, settings, lang_version)
120
+ if _placements(masked.text) != _placements(formatted):
121
+ raise FormattingError("a line the parser cannot read would move")
122
+ kept.extend(masked.hidden.values())
123
+ return conditional.restore(formatted, masked.hidden, reindent=True, nested=True)
124
+
125
+
126
+ def _usable_around(text: str) -> bool:
127
+ """A view that parses, or would once its fewest unreadable lines are hidden."""
128
+ return conditional.parses(text) or isolate(text) is not None
129
+
130
+
131
+ def _source_rows(source: str, kept: list[str]) -> tuple[int, ...]:
132
+ """The source lines (1-based) whose text was left as written in some view."""
133
+ texts = {line.strip() for hidden in kept for line in hidden.splitlines() if line.strip()}
134
+ return tuple(number for number, line in enumerate(source.splitlines(), start=1) if line.strip() in texts)
135
+
136
+
102
137
  def _format_around(source: str, settings: Settings, lang_version: float) -> FormatResult | None:
103
138
  """Format all but the fewest lines that keep the source from parsing."""
104
139
  masked = isolate(source)
@@ -138,12 +173,28 @@ def _placements(text: str) -> list[tuple[str, str, str]]:
138
173
  return out
139
174
 
140
175
 
141
- def _same_view(before: str, after: str) -> bool:
142
- """Whether two views of a file, with the same branches hidden, are the same code."""
176
+ def _same_view(before: str, after: str, *, around: bool = False) -> bool:
177
+ """Whether two views of a file, with the same branches hidden, are the same code.
178
+
179
+ With ``around``, views that hold unreadable lines are compared with those
180
+ lines hidden in both, and the hidden lines must match apart from indentation.
181
+ """
143
182
  try:
144
183
  old, new = build(before), build(after)
145
184
  except CSharpSyntaxError:
146
- return False
185
+ if not around:
186
+ return False
187
+ first = isolate(before)
188
+ if first is None:
189
+ return False
190
+ # The same lines, found by their text, are hidden in the formatted view.
191
+ second = _hide_matching(after, _stripped(first.hidden.values()))
192
+ if second is None:
193
+ return False
194
+ try:
195
+ old, new = build(first.text), build(second.text)
196
+ except CSharpSyntaxError:
197
+ return False
147
198
  if canonical(old.root) != canonical(new.root):
148
199
  return False
149
200
  # Hidden text differs between the views by design: it was formatted in its own view.
@@ -208,3 +259,21 @@ def _run_with_big_stack(work: Callable[[], T]) -> T:
208
259
  if failure:
209
260
  raise failure[0]
210
261
  return result[0]
262
+
263
+
264
+ def _stripped(texts: Iterable[str]) -> list[str]:
265
+ return [line.strip() for text in texts for line in text.splitlines()]
266
+
267
+
268
+ def _hide_matching(text: str, wanted: list[str]) -> conditional.Masked | None:
269
+ """Hide the lines of ``text`` whose stripped text is ``wanted``, in order; None if any is missing."""
270
+ lines = conditional._split_lines(text)
271
+ keep = [True] * len(lines)
272
+ position = 0
273
+ for number, line in enumerate(lines):
274
+ if position < len(wanted) and line.strip() == wanted[position]:
275
+ keep[number] = False
276
+ position += 1
277
+ if position != len(wanted):
278
+ return None
279
+ return conditional.hide(lines, keep, salt="isolate:")
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from tree_sitter import Node as TSNode
18
18
 
19
- from prettycsharp.conditional import Masked, _content, _directive, _split_lines, hide
19
+ from prettycsharp.conditional import Masked, _content, _directive, _split_lines, hide, is_placeholder
20
20
  from prettycsharp.parsing import parse_tree
21
21
 
22
22
  # Limits that keep the search quick on any file.
@@ -38,7 +38,7 @@ def isolate(source: str) -> Masked | None:
38
38
  if count == 0:
39
39
  if all(keep) or not _worth_it(lines, keep):
40
40
  return None
41
- masked = hide(lines, keep, reindent=True)
41
+ masked = hide(lines, keep, reindent=True, salt="isolate:")
42
42
  masked.hidden_rows = tuple(number + 1 for number, kept in enumerate(keep) if not kept)
43
43
  return masked
44
44
  if span is None:
@@ -80,8 +80,12 @@ def _worth_it(lines: list[str], keep: list[bool]) -> bool:
80
80
 
81
81
 
82
82
  def _protected(line: str) -> bool:
83
- """#if, #elif, #else and #endif lines hold the branches together: never hidden."""
84
- found = _directive(_content(line))
83
+ """Lines never hidden: #if, #elif, #else and #endif hold the branches
84
+ together, and a placeholder already stands for hidden text."""
85
+ content = _content(line)
86
+ if is_placeholder(content):
87
+ return True
88
+ found = _directive(content)
85
89
  return found is not None and found[0] in ("if", "elif", "else", "endif")
86
90
 
87
91
 
@@ -733,6 +733,9 @@ def remove_trailing_commas(root: Node) -> None:
733
733
  close = node.children[-1]
734
734
  if comma.text != "," or comma.named or close.text not in ("}", "]"):
735
735
  continue
736
+ if any(is_placeholder(c.text) for c in [*comma.trailing, *close.leading]):
737
+ # Hidden lines follow: in another #if view the comma separates items.
738
+ continue
736
739
  before = node.children[-3].last_token()
737
740
  if before is None:
738
741
  continue
@@ -70,9 +70,9 @@ class Settings:
70
70
  simple_lists_inline: bool = True
71
71
  trailing_commas: str = "remove"
72
72
 
73
- align_switch_arms: bool = True
74
- align_assignments: bool = True
75
- align_enum_values: bool = True
73
+ align_switch_arms: bool = False
74
+ align_assignments: bool = False
75
+ align_enum_values: bool = False
76
76
 
77
77
  switch_case_indent: bool = True
78
78
  switch_case_blank_lines: bool = True
@@ -233,6 +233,50 @@ def test_a_damaged_branch_is_refused(monkeypatch: pytest.MonkeyPatch) -> None:
233
233
  format_source("#if NET\nclass C : IAsyncDisposable\n#else\nclass C\n#endif\n{ }\n")
234
234
 
235
235
 
236
+ def test_comma_before_a_hidden_branch_is_not_a_trailing_comma() -> None:
237
+ check(
238
+ """
239
+ class C
240
+ {
241
+ #if NET
242
+ class D : I
243
+ #else
244
+ class D
245
+ #endif
246
+ {
247
+ }
248
+ static readonly HashSet<Type> Types = new()
249
+ {
250
+ typeof(Guid), typeof(Uri),
251
+ #if DATE_ONLY
252
+ typeof(DateOnly)
253
+ #endif
254
+ };
255
+ }
256
+ """,
257
+ """
258
+ class C
259
+ {
260
+ #if NET
261
+ class D : I
262
+ #else
263
+ class D
264
+ #endif
265
+ { }
266
+
267
+ static readonly HashSet<Type> Types = new()
268
+ {
269
+ typeof(Guid),
270
+ typeof(Uri),
271
+ #if DATE_ONLY
272
+ typeof(DateOnly)
273
+ #endif
274
+ };
275
+ }
276
+ """,
277
+ )
278
+
279
+
236
280
  def test_property_split_between_arrow_and_accessors_under_block_bodies() -> None:
237
281
  check(
238
282
  """
@@ -63,6 +63,45 @@ def test_genuinely_broken_code_that_no_line_rescues_is_still_refused() -> None:
63
63
  format_source("class {\n{\n{\n", AROUND)
64
64
 
65
65
 
66
+ SPLIT_AND_NEW = """#if NET
67
+ public sealed class Pool : IAsyncDisposable
68
+ #else
69
+ public sealed class Pool : IDisposable
70
+ #endif
71
+ {
72
+ static public int Size=3;
73
+ void M()
74
+ {
75
+ a?.B = 1;
76
+ Run( );
77
+ }
78
+ }
79
+ """
80
+
81
+
82
+ def test_format_around_works_inside_each_if_view() -> None:
83
+ result = format_details(SPLIT_AND_NEW, AROUND)
84
+ assert result.text == (
85
+ "#if NET\npublic sealed class Pool : IAsyncDisposable\n#else\npublic sealed class Pool : IDisposable\n#endif\n"
86
+ "{\n public static int Size = 3;\n\n void M()\n {\n a?.B = 1;\n Run();\n }\n}\n"
87
+ )
88
+ assert result.kept_lines == (10,)
89
+ assert format_source(result.text, AROUND) == result.text
90
+
91
+
92
+ def test_split_and_new_syntax_is_still_skipped_by_default() -> None:
93
+ with pytest.raises(CSharpSyntaxError):
94
+ format_source(SPLIT_AND_NEW)
95
+
96
+
97
+ def test_one_line_member_with_new_syntax_keeps_only_that_line() -> None:
98
+ source = SPLIT_AND_NEW.replace(" void M()\n {\n a?.B = 1;\n Run( );\n }\n", " void M() { a?.B = 1; Run( ); }\n")
99
+ result = format_details(source, AROUND)
100
+ assert " public static int Size = 3;" in result.text
101
+ assert " void M() { a?.B = 1; Run( ); }" in result.text
102
+ assert result.kept_lines == (8,)
103
+
104
+
66
105
  def test_command_line_reports_the_kept_lines(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
67
106
  path = tmp_path / "C.cs"
68
107
  path.write_text(NEW_SYNTAX)
@@ -131,7 +131,6 @@ def test_single_call_and_property_chains_stay_on_one_line() -> None:
131
131
  var city = order.Customer.Address.City;
132
132
  var zip = order.Customer.Address.GetPostalCode();
133
133
  """,
134
- align_assignments=False,
135
134
  )
136
135
 
137
136
 
@@ -230,7 +229,7 @@ def test_fields_grouped_tight_and_blank_lines_around_members_with_bodies() -> No
230
229
 
231
230
  public Order(int id, string name)
232
231
  {
233
- _id = id;
232
+ _id = id;
234
233
  _name = name;
235
234
  }
236
235
 
@@ -429,7 +428,6 @@ def test_simple_value_lists_stay_inline_and_complex_ones_break() -> None:
429
428
  new Point(1, 1)
430
429
  };
431
430
  """,
432
- align_assignments=False,
433
431
  )
434
432
 
435
433
 
@@ -513,7 +511,35 @@ def test_switch_sections_are_indented_with_blank_lines_between() -> None:
513
511
  )
514
512
 
515
513
 
516
- def test_switch_expression_arms_align_their_arrows() -> None:
514
+ def test_nothing_is_padded_into_columns() -> None:
515
+ check(
516
+ """
517
+ var label = status switch { Status.Paid => "Paid", Status.Pending => "Pending", _ => "Unknown" };
518
+ var id = order.Id;
519
+ var customer = order.Customer;
520
+ public enum Status { Pending = 0, Paid = 1, Cancelled = 2 }
521
+ """,
522
+ """
523
+ var label = status switch
524
+ {
525
+ Status.Paid => "Paid",
526
+ Status.Pending => "Pending",
527
+ _ => "Unknown"
528
+ };
529
+ var id = order.Id;
530
+ var customer = order.Customer;
531
+
532
+ public enum Status
533
+ {
534
+ Pending = 0,
535
+ Paid = 1,
536
+ Cancelled = 2
537
+ }
538
+ """,
539
+ )
540
+
541
+
542
+ def test_switch_expression_arms_align_when_turned_on() -> None:
517
543
  check(
518
544
  """
519
545
  var label = status switch { Status.Paid => "Paid", Status.Pending => "Pending", _ => "Unknown" };
@@ -526,10 +552,11 @@ def test_switch_expression_arms_align_their_arrows() -> None:
526
552
  _ => "Unknown"
527
553
  };
528
554
  """,
555
+ align_switch_arms=True,
529
556
  )
530
557
 
531
558
 
532
- def test_consecutive_assignments_and_enum_values_align() -> None:
559
+ def test_assignments_and_enum_values_align_when_turned_on() -> None:
533
560
  check(
534
561
  """
535
562
  var id = order.Id;
@@ -551,6 +578,8 @@ def test_consecutive_assignments_and_enum_values_align() -> None:
551
578
  Cancelled = 2
552
579
  }
553
580
  """,
581
+ align_assignments=True,
582
+ align_enum_values=True,
554
583
  )
555
584
 
556
585
 
@@ -797,7 +826,6 @@ def test_raw_and_verbatim_strings_are_untouched() -> None:
797
826
  """;
798
827
  var path = @"C:\\orders\\ ";
799
828
  ''',
800
- align_assignments=False,
801
829
  )
802
830
 
803
831
 
@@ -262,7 +262,6 @@ def test_parentheses_remove_redundant_follows_precedence() -> None:
262
262
  var m = n - (o - p);
263
263
  """,
264
264
  parentheses="remove_redundant",
265
- align_assignments=False,
266
265
  )
267
266
 
268
267
 
@@ -238,13 +238,15 @@ def test_initializer_break_threshold_zero_and_simple_lists() -> None:
238
238
  assert fmt("var a = new[] { 1, 2 };\n", simple_lists_inline=False) == "var a = new[]\n{\n 1,\n 2\n};\n"
239
239
 
240
240
 
241
- def test_alignment_off() -> None:
241
+ def test_alignment_is_off_by_default_and_each_setting_turns_it_on() -> None:
242
242
  source = "var id = order.Id;\nvar customer = order.Customer;\n"
243
- assert fmt(source, align_assignments=False) == source
244
- assert fmt(
245
- "var l = s switch { 1 => \"a\", 22 => \"b\" };\n", align_switch_arms=False
246
- ) == 'var l = s switch\n{\n 1 => "a",\n 22 => "b"\n};\n'
247
- assert fmt("enum E { A = 1, Bb = 2 }\n", align_enum_values=False) == "enum E\n{\n A = 1,\n Bb = 2\n}\n"
243
+ assert fmt(source) == source
244
+ assert fmt(source, align_assignments=True) == "var id = order.Id;\nvar customer = order.Customer;\n"
245
+ switch = 'var l = s switch { 1 => "a", 22 => "b" };\n'
246
+ assert fmt(switch) == 'var l = s switch\n{\n 1 => "a",\n 22 => "b"\n};\n'
247
+ assert fmt(switch, align_switch_arms=True) == 'var l = s switch\n{\n 1 => "a",\n 22 => "b"\n};\n'
248
+ assert fmt("enum E { A = 1, Bb = 2 }\n") == "enum E\n{\n A = 1,\n Bb = 2\n}\n"
249
+ assert fmt("enum E { A = 1, Bb = 2 }\n", align_enum_values=True) == "enum E\n{\n A = 1,\n Bb = 2\n}\n"
248
250
 
249
251
 
250
252
  def test_switch_case_indent_and_blank_lines_off() -> None:
File without changes
File without changes
File without changes
File without changes
File without changes