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.
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/CHANGELOG.md +21 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/DESIGN.md +18 -9
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/PKG-INFO +6 -5
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/README.md +5 -4
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/__init__.py +1 -1
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/conditional.py +30 -18
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/formatter.py +80 -11
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/isolate.py +8 -4
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/rewrites.py +3 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/settings.py +3 -3
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_conditional.py +44 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_format_around.py +39 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_layout.py +34 -6
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_rewrites.py +0 -1
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_settings.py +8 -6
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.gitattributes +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/codeql/codeql-config.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/dependabot.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/build.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/build.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/fuzz.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/fuzz.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/pip-audit.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/pip-audit.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/runtime.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/runtime.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/test.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/test.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/zizmor.in +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/requirements/zizmor.txt +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/accepted.toml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/clamav/Dockerfile +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/malware-accepted.toml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/semgrep/Dockerfile +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/yara.json +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/security/yara_update.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/auto-merge.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/ci.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/fuzz.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/malware-scan.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/publish.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/scorecard.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/security.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/workflows/update-yara-rules.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.github/zizmor.yml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/.gitignore +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/AGENTS.md +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/CLAUDE.md +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/LICENSE +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/SECURITY.md +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/fuzz/fuzz_formatter.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/pyproject.toml +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/scripts/security/malware_report.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/scripts/security/security_report.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/__main__.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/cli.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/config.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/doc.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/ignore.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/parsing.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/__init__.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/core.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/declarations.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/expressions.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/printer/statements.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/project.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/py.typed +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/syntax.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/src/prettycsharp/verify.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/directives.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/members.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/order.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/statements.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/fuzz_corpus/csharp/strings.cs +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/support.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_cli.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_config.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_doc.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_docs.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_fuzz_corpus.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_ignore.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_parsing.py +0 -0
- {prettycsharp-1.1.0 → prettycsharp-1.1.2}/tests/test_project.py +0 -0
- {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` | `
|
|
237
|
-
| `align_assignments` | `true`: consecutive local assignments and field initializers |
|
|
238
|
-
| `align_enum_values` | `
|
|
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
|
-
|
|
241
|
-
|
|
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,
|
|
348
|
-
|
|
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
|
|
374
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
205
|
-
|
|
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.
|
|
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.
|
|
226
|
-
|
|
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
|
|
341
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
369
|
-
ConditionalError when the result, seen through any
|
|
370
|
-
used, is not the same code as the source
|
|
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,
|
|
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
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
return
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
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 =
|
|
74
|
-
align_assignments: bool =
|
|
75
|
-
align_enum_values: bool =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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
|
|
244
|
-
assert fmt(
|
|
245
|
-
|
|
246
|
-
) == 'var l = s switch\n{\n 1 => "a",\n 22 => "b"\n};\n'
|
|
247
|
-
assert fmt(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|