prettycsharp 1.0.0__tar.gz → 1.1.1__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 (91) hide show
  1. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/AGENTS.md +6 -1
  2. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/CHANGELOG.md +33 -0
  3. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/DESIGN.md +55 -4
  4. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/PKG-INFO +41 -8
  5. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/README.md +40 -7
  6. prettycsharp-1.1.1/src/prettycsharp/__init__.py +18 -0
  7. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/cli.py +7 -2
  8. prettycsharp-1.1.1/src/prettycsharp/conditional.py +399 -0
  9. prettycsharp-1.1.1/src/prettycsharp/formatter.py +279 -0
  10. prettycsharp-1.1.1/src/prettycsharp/isolate.py +118 -0
  11. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/parsing.py +12 -5
  12. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/printer/core.py +9 -3
  13. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/rewrites.py +19 -2
  14. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/settings.py +2 -0
  15. prettycsharp-1.1.1/tests/fuzz_corpus/csharp/split_by_conditionals.cs +17 -0
  16. prettycsharp-1.1.1/tests/test_conditional.py +333 -0
  17. prettycsharp-1.1.1/tests/test_format_around.py +110 -0
  18. prettycsharp-1.0.0/src/prettycsharp/__init__.py +0 -9
  19. prettycsharp-1.0.0/src/prettycsharp/formatter.py +0 -106
  20. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.gitattributes +0 -0
  21. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/codeql/codeql-config.yml +0 -0
  22. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/dependabot.yml +0 -0
  23. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/build.in +0 -0
  24. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/build.txt +0 -0
  25. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/fuzz.in +0 -0
  26. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/fuzz.txt +0 -0
  27. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/pip-audit.in +0 -0
  28. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/pip-audit.txt +0 -0
  29. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/runtime.in +0 -0
  30. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/runtime.txt +0 -0
  31. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/test.in +0 -0
  32. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/test.txt +0 -0
  33. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/zizmor.in +0 -0
  34. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/requirements/zizmor.txt +0 -0
  35. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/accepted.toml +0 -0
  36. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/clamav/Dockerfile +0 -0
  37. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/malware-accepted.toml +0 -0
  38. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/semgrep/Dockerfile +0 -0
  39. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/yara.json +0 -0
  40. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/security/yara_update.py +0 -0
  41. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/auto-merge.yml +0 -0
  42. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/ci.yml +0 -0
  43. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/fuzz.yml +0 -0
  44. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/malware-scan.yml +0 -0
  45. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/publish.yml +0 -0
  46. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/scorecard.yml +0 -0
  47. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/security.yml +0 -0
  48. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/workflows/update-yara-rules.yml +0 -0
  49. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.github/zizmor.yml +0 -0
  50. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/.gitignore +0 -0
  51. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/CLAUDE.md +0 -0
  52. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/LICENSE +0 -0
  53. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/SECURITY.md +0 -0
  54. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/fuzz/fuzz_formatter.py +0 -0
  55. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/pyproject.toml +0 -0
  56. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/scripts/security/malware_report.py +0 -0
  57. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/scripts/security/security_report.py +0 -0
  58. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/__main__.py +0 -0
  59. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/config.py +0 -0
  60. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/doc.py +0 -0
  61. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/ignore.py +0 -0
  62. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/printer/__init__.py +0 -0
  63. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/printer/declarations.py +0 -0
  64. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/printer/expressions.py +0 -0
  65. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/printer/statements.py +0 -0
  66. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/project.py +0 -0
  67. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/py.typed +0 -0
  68. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/syntax.py +0 -0
  69. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/src/prettycsharp/verify.py +0 -0
  70. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
  71. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/directives.cs +0 -0
  72. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
  73. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
  74. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/members.cs +0 -0
  75. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/order.cs +0 -0
  76. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
  77. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/statements.cs +0 -0
  78. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/fuzz_corpus/csharp/strings.cs +0 -0
  79. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/support.py +0 -0
  80. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_cli.py +0 -0
  81. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_config.py +0 -0
  82. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_doc.py +0 -0
  83. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_docs.py +0 -0
  84. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_fuzz_corpus.py +0 -0
  85. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_ignore.py +0 -0
  86. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_layout.py +0 -0
  87. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_parsing.py +0 -0
  88. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_project.py +0 -0
  89. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_rewrites.py +0 -0
  90. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_settings.py +0 -0
  91. {prettycsharp-1.0.0 → prettycsharp-1.1.1}/tests/test_verify.py +0 -0
@@ -10,7 +10,12 @@ token-changing rewrites; `printer/` turns nodes into a layout document
10
10
  (`core.py` dispatch, tokens, comments and lists; `expressions.py`,
11
11
  `statements.py` and `declarations.py` the handlers, each `p_<node type>`);
12
12
  `doc.py` fits the document to the width; `verify.py` reparses the output and
13
- compares canonical trees. `settings.py`, `config.py`, `project.py`,
13
+ compares canonical trees. When `#if` branches split code so the file does
14
+ not parse whole, `conditional.py` formats one view (set of defined symbols)
15
+ at a time, with directive lines and inactive branches behind placeholder
16
+ comments, and checks every view at the end. With `syntax_errors =
17
+ "format_around"`, `isolate.py` finds the fewest lines whose removal lets the
18
+ file parse and hides them behind the same placeholders. `settings.py`, `config.py`, `project.py`,
14
19
  `ignore.py` and `cli.py` are the settings, files and command line.
15
20
 
16
21
  ## Commands
@@ -2,6 +2,39 @@
2
2
 
3
3
  Each release's section becomes the body of its GitHub release.
4
4
 
5
+ ## [1.1.1] - 2026-10-17
6
+
7
+ - `syntax_errors = "format_around"` now works inside each view of a file
8
+ split by `#if`: a view that does not parse is formatted around its own
9
+ unreadable lines, so both branches of a split class header are formatted
10
+ even when a method holds syntax newer than the parser. Before, the
11
+ fallback ran on the whole file and kept the branch lines as written.
12
+ - A comma followed only by hidden `#if` lines before the closing brace is no
13
+ longer removed as a trailing comma, since in another view it separates
14
+ items. Files like this were skipped before; on ten open-source
15
+ repositories the default settings now skip 11 files instead of 14, and
16
+ `format_around` skips 2.
17
+
18
+ ## [1.1.0] - 2026-10-17
19
+
20
+ - Files whose `#if` branches split a declaration or statement, such as a
21
+ class header with a different base list in each branch, are formatted
22
+ instead of refused. Each view of the file (a set of defined symbols) is
23
+ formatted on its own with the other branches set aside, so every branch
24
+ is formatted, and the result is checked through every view. On ten
25
+ open-source repositories this formats 94 of the 108 files that 1.0.0
26
+ skipped.
27
+ - A file that ends in a directive without a final line break, such as
28
+ `#pragma warning restore`, parses.
29
+ - A new setting, `syntax_errors = "format_around"`, formats files that hold
30
+ syntax newer than the parser, such as C# 14's null-conditional assignment
31
+ or the `#:` directives of file-based apps. The fewest lines the parser
32
+ cannot read are left as written, moved to the new indentation, and the
33
+ rest is formatted; the command line names the lines it kept. The default,
34
+ `skip_file`, leaves such files untouched as before.
35
+ - `format_details()` returns the formatted text with the lines left as
36
+ written.
37
+
5
38
  ## [1.0.0] - 2026-10-17
6
39
 
7
40
  The first release: a complete formatter for C# files, projects and
@@ -322,9 +322,60 @@ On by default:
322
322
 
323
323
  `--no-ignore` turns off both ignore files for one run.
324
324
 
325
+ ## Conditional compilation
326
+
327
+ tree-sitter's C# grammar parses every `#if` branch as code. When the
328
+ branches together are valid C#, the file is formatted as a whole. When they
329
+ are not, because a declaration or statement is split between branches, the
330
+ file is formatted one view at a time (`conditional.py`):
331
+
332
+ 1. A view is a set of defined symbols. Conditions are evaluated as the
333
+ compiler does (`!`, `&&`, `||`, `==`, `!=`, parentheses, `true`, `false`),
334
+ with `#define` and `#undef` honoured where they are compiled.
335
+ 2. In a view, each directive line and each run of inactive lines is replaced
336
+ by a placeholder comment. What is left is the code the compiler sees, and
337
+ it is formatted normally, verification included. The placeholders then go
338
+ back to the text they stood for, exactly as written.
339
+ 3. Views are tried simplest first (no symbols, each symbol alone, all, all
340
+ but one, at most 16) and kept when they parse and make a branch active
341
+ that no earlier view did, so every branch is formatted in its own view.
342
+ 4. The result is checked through every view used: with the same lines set
343
+ aside, it must be the same code as the source. Otherwise the file is left
344
+ untouched.
345
+
346
+ 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, modifiers split by
348
+ one keep their order, and a comma with hidden lines between it and the
349
+ closing brace is not taken for a trailing comma.
350
+
351
+ With `syntax_errors = "format_around"` (below), a view that does not parse
352
+ is still used when hiding its fewest unreadable lines makes it parse: the
353
+ view is formatted around those lines, and the final check hides the same
354
+ lines, found by their text, in the view of the source and of the result.
355
+
356
+ ## Code the parser cannot read
357
+
358
+ | Setting | Default | Alternatives |
359
+ | --- | --- | --- |
360
+ | `syntax_errors` | `skip_file`: a file that does not parse is reported and left untouched | `format_around` |
361
+
362
+ Syntax newer than the grammar, such as C# 14's null-conditional assignment
363
+ or the `#:` directives of file-based apps, does not parse. With
364
+ `format_around`, the formatter looks for the fewest whole lines whose removal
365
+ lets the file parse (`isolate.py`), trying the lines around each error in
366
+ turn. Those lines are hidden behind placeholder comments, the rest is
367
+ formatted and verified, and they go back exactly as written, moved to the
368
+ indentation their placeholder was printed at (unless they could hold a
369
+ multi-line string). Each hidden line must sit between the same two code
370
+ tokens before and after formatting, so the order of the code cannot change.
371
+ The command line names the lines left as written.
372
+
373
+ No more than half a file's lines are hidden, and some code must be left to
374
+ format; otherwise the file is skipped as before.
375
+
325
376
  ## Limits
326
377
 
327
- tree-sitter's C# grammar parses every `#if` branch as code, so a declaration
328
- or statement split between `#if` and `#else` does not parse. Neither does
329
- syntax newer than the grammar. Such files are reported with their line and
330
- column and left untouched.
378
+ A file still is not formatted when no view of its `#if` branches parses and,
379
+ with `format_around`, hiding lines makes neither a view nor the whole file
380
+ parse. Lines are hidden whole, so a one-line member that holds unreadable
381
+ syntax is kept as written in full.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prettycsharp
3
- Version: 1.0.0
3
+ Version: 1.1.1
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
@@ -147,10 +147,14 @@ and left untouched.
147
147
  From Python:
148
148
 
149
149
  ```python
150
- from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_source
150
+ from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_details, format_source
151
151
 
152
152
  formatted = format_source(source)
153
153
  narrow = format_source(source, Settings().with_values({"max_line_length": 100}))
154
+
155
+ # format_details also says which lines were left as written (format_around).
156
+ result = format_details(source, Settings().with_values({"syntax_errors": "format_around"}))
157
+ print(result.text, result.kept_lines)
154
158
  ```
155
159
 
156
160
  ## Settings
@@ -199,13 +203,42 @@ same words. If anything else differs, the file is not changed and the
199
203
  command reports the difference. String literals, including verbatim, raw and
200
204
  interpolated strings, are printed exactly as written.
201
205
 
202
- ## Limits
206
+ ## Conditional compilation
207
+
208
+ `#if` branches can split a declaration, as in:
209
+
210
+ ```csharp
211
+ #if NET10_0_OR_GREATER
212
+ public sealed class Pool : IAsyncDisposable
213
+ #else
214
+ public sealed class Pool : IDisposable
215
+ #endif
216
+ {
217
+ ```
218
+
219
+ Such a file is formatted the way the compiler reads it, one set of defined
220
+ symbols at a time: the directive lines and the inactive branches are set
221
+ aside, the rest is formatted, and they are put back as written. Symbol sets
222
+ are chosen so that every branch is active in one of them, so every branch
223
+ gets formatted. The result is checked under each symbol set, and a file
224
+ where any branch would change in meaning is left untouched.
225
+
226
+ ## Code the parser cannot read
227
+
228
+ prettycsharp reads C# through tree-sitter's C# grammar, and syntax newer
229
+ than the grammar, such as C# 14's null-conditional assignment, does not
230
+ parse. By default such a file is reported with its line and column and left
231
+ untouched. With `syntax_errors = "format_around"`, the formatter finds the
232
+ fewest lines it cannot read, leaves just those as written, formats the rest,
233
+ and names the lines it kept:
234
+
235
+ ```bash
236
+ prettycsharp format src/ -o syntax_errors=format_around
237
+ ```
203
238
 
204
- prettycsharp reads C# through tree-sitter's C# grammar, which parses every
205
- `#if` branch as code. A declaration or statement split between `#if` and
206
- `#else` therefore does not parse, and neither does syntax newer than the
207
- grammar (such as C# 14's null-conditional assignment). Such files are
208
- reported with their line and column and left untouched.
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.
209
242
 
210
243
  ## License
211
244
 
@@ -112,10 +112,14 @@ and left untouched.
112
112
  From Python:
113
113
 
114
114
  ```python
115
- from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_source
115
+ from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_details, format_source
116
116
 
117
117
  formatted = format_source(source)
118
118
  narrow = format_source(source, Settings().with_values({"max_line_length": 100}))
119
+
120
+ # format_details also says which lines were left as written (format_around).
121
+ result = format_details(source, Settings().with_values({"syntax_errors": "format_around"}))
122
+ print(result.text, result.kept_lines)
119
123
  ```
120
124
 
121
125
  ## Settings
@@ -164,13 +168,42 @@ same words. If anything else differs, the file is not changed and the
164
168
  command reports the difference. String literals, including verbatim, raw and
165
169
  interpolated strings, are printed exactly as written.
166
170
 
167
- ## Limits
171
+ ## Conditional compilation
172
+
173
+ `#if` branches can split a declaration, as in:
174
+
175
+ ```csharp
176
+ #if NET10_0_OR_GREATER
177
+ public sealed class Pool : IAsyncDisposable
178
+ #else
179
+ public sealed class Pool : IDisposable
180
+ #endif
181
+ {
182
+ ```
183
+
184
+ Such a file is formatted the way the compiler reads it, one set of defined
185
+ symbols at a time: the directive lines and the inactive branches are set
186
+ aside, the rest is formatted, and they are put back as written. Symbol sets
187
+ are chosen so that every branch is active in one of them, so every branch
188
+ gets formatted. The result is checked under each symbol set, and a file
189
+ where any branch would change in meaning is left untouched.
190
+
191
+ ## Code the parser cannot read
192
+
193
+ prettycsharp reads C# through tree-sitter's C# grammar, and syntax newer
194
+ than the grammar, such as C# 14's null-conditional assignment, does not
195
+ parse. By default such a file is reported with its line and column and left
196
+ untouched. With `syntax_errors = "format_around"`, the formatter finds the
197
+ fewest lines it cannot read, leaves just those as written, formats the rest,
198
+ and names the lines it kept:
199
+
200
+ ```bash
201
+ prettycsharp format src/ -o syntax_errors=format_around
202
+ ```
168
203
 
169
- prettycsharp reads C# through tree-sitter's C# grammar, which parses every
170
- `#if` branch as code. A declaration or statement split between `#if` and
171
- `#else` therefore does not parse, and neither does syntax newer than the
172
- grammar (such as C# 14's null-conditional assignment). Such files are
173
- reported with their line and column and left untouched.
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.
174
207
 
175
208
  ## License
176
209
 
@@ -0,0 +1,18 @@
1
+ """An opinionated formatter for C#, in the spirit of Prettier."""
2
+
3
+ from prettycsharp.formatter import FormatResult, FormattingError, format_details, format_source
4
+ from prettycsharp.parsing import CSharpSyntaxError
5
+ from prettycsharp.settings import Settings, SettingsError
6
+
7
+ __version__ = "1.1.1"
8
+
9
+ __all__ = [
10
+ "CSharpSyntaxError",
11
+ "FormatResult",
12
+ "FormattingError",
13
+ "Settings",
14
+ "SettingsError",
15
+ "__version__",
16
+ "format_details",
17
+ "format_source",
18
+ ]
@@ -25,7 +25,7 @@ from pathlib import Path
25
25
 
26
26
  from prettycsharp import __version__, config, ignore, project
27
27
  from prettycsharp.config import settings_for
28
- from prettycsharp.formatter import FormattingError, format_source
28
+ from prettycsharp.formatter import FormattingError, format_details
29
29
  from prettycsharp.ignore import Ignorer
30
30
  from prettycsharp.parsing import CSharpSyntaxError
31
31
  from prettycsharp.project import compiled_files, lang_version_for, solution_projects
@@ -163,7 +163,12 @@ def _format_text(text: str, path: Path, settings: Settings) -> str:
163
163
  """Format text read from ``path``, keeping or changing its byte order mark."""
164
164
  bom = _BOM if text.startswith(_BOM) else ""
165
165
  lang = lang_version_for(path) if settings.lang_version == "auto" else None
166
- formatted = format_source(text[len(bom) :], settings, lang_version=lang)
166
+ result = format_details(text[len(bom) :], settings, lang_version=lang)
167
+ formatted = result.text
168
+ if result.kept_lines:
169
+ numbers = ", ".join(str(n) for n in result.kept_lines)
170
+ noun = "line" if len(result.kept_lines) == 1 else "lines"
171
+ print(f"{path}: {noun} {numbers} left as written; the parser cannot read them", file=sys.stderr)
167
172
  if settings.charset == "utf-8-bom":
168
173
  bom = _BOM
169
174
  elif settings.charset == "utf-8":