prettycsharp 1.1.7__tar.gz → 1.1.9__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.7 → prettycsharp-1.1.9}/CHANGELOG.md +26 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/DESIGN.md +25 -2
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/PKG-INFO +5 -4
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/README.md +4 -3
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/__init__.py +1 -1
- prettycsharp-1.1.9/src/prettycsharp/parsing.py +135 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/printer/expressions.py +67 -10
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/printer/statements.py +7 -2
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/syntax.py +44 -0
- prettycsharp-1.1.9/tests/fuzz_corpus/csharp/conditional_assignments.cs +13 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_format_around.py +17 -17
- prettycsharp-1.1.9/tests/test_grammar_compatibility.py +102 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_layout.py +162 -3
- prettycsharp-1.1.7/src/prettycsharp/parsing.py +0 -60
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.gitattributes +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/codeql/codeql-config.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/dependabot.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/build.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/build.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/fuzz.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/fuzz.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/pip-audit.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/pip-audit.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/runtime.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/runtime.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/test.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/test.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/zizmor.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/requirements/zizmor.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/accepted.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/clamav/Dockerfile +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/malware-accepted.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/semgrep/Dockerfile +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/yara.json +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/security/yara_update.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/auto-merge.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/ci.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/fuzz.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/malware-scan.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/publish.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/scorecard.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/security.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/workflows/update-yara-rules.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.github/zizmor.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/.gitignore +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/AGENTS.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/CLAUDE.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/LICENSE +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/SECURITY.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/fuzz/fuzz_formatter.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/pyproject.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/scripts/security/malware_report.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/scripts/security/security_report.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/__main__.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/cli.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/conditional.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/config.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/doc.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/formatter.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/ignore.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/isolate.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/printer/__init__.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/printer/core.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/printer/declarations.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/project.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/py.typed +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/rewrites.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/settings.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/src/prettycsharp/verify.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/comment_only_accessor_list.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/directives.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/members.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/order.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/statements.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/fuzz_corpus/csharp/strings.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/support.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_cli.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_conditional.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_config.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_doc.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_docs.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_fuzz_corpus.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_ignore.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_parsing.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_project.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_rewrites.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_settings.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.9}/tests/test_verify.py +0 -0
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Each release's section becomes the body of its GitHub release.
|
|
4
4
|
|
|
5
|
+
## [1.1.9] - 2026-10-18
|
|
6
|
+
|
|
7
|
+
- With the default `binary_wrap`, multiline conditions in catch filters,
|
|
8
|
+
`if`, and loops start below `(` and put `)` on its own line. This includes
|
|
9
|
+
pattern chains, mixed Boolean conditions, and predicate calls.
|
|
10
|
+
- Inline collection expressions keep tight brackets around complex elements
|
|
11
|
+
and spreads, such as `[new HpackField(wire, "")]` and `[.. source.Select(...)]`,
|
|
12
|
+
avoiding Visual Studio's IDE0055 formatting diagnostic.
|
|
13
|
+
- A multiline collection argument no longer adds a blank line after the
|
|
14
|
+
call's opening parenthesis.
|
|
15
|
+
- Chains of negated patterns, including `is not A and not B` followed by
|
|
16
|
+
Boolean operators, wrap at one consistent continuation indentation level.
|
|
17
|
+
|
|
18
|
+
## [1.1.8] - 2026-10-18
|
|
19
|
+
|
|
20
|
+
- Long `or` and `and` patterns wrap at the configured line width using
|
|
21
|
+
`binary_wrap`, while short patterns stay inline.
|
|
22
|
+
- A multiline property pattern moves its opening brace onto its own line
|
|
23
|
+
with Allman braces, including patterns in catch filters, avoiding IDE0055.
|
|
24
|
+
- Assignments to a variable named `required`, including compound assignments,
|
|
25
|
+
format without mistaking the identifier for a member modifier.
|
|
26
|
+
- C# 14 null-conditional member and element assignments, such as
|
|
27
|
+
`telemetry?.Route = value` and `items?[index] += value`, format instead of
|
|
28
|
+
being refused. Original tokens, comments, and conditional-access semantics
|
|
29
|
+
are preserved, and malformed input is still refused.
|
|
30
|
+
|
|
5
31
|
## [1.1.7] - 2026-10-18
|
|
6
32
|
|
|
7
33
|
- Comments after the final `switch` section align with its statements
|
|
@@ -108,6 +108,9 @@ stops the search, and the file nearest the source wins.
|
|
|
108
108
|
|
|
109
109
|
Short auto-property accessors always stay on one line: `public int Id { get; set; }`.
|
|
110
110
|
|
|
111
|
+
Property patterns stay inline when they fit. When broken across lines,
|
|
112
|
+
their opening brace follows `brace_style` for statements.
|
|
113
|
+
|
|
111
114
|
```csharp
|
|
112
115
|
if (order.IsPaid)
|
|
113
116
|
{
|
|
@@ -174,6 +177,14 @@ parenthesis unless all three clauses are empty: `for (; ; )`.
|
|
|
174
177
|
| `ternary_wrap` | `before`: break before `?` and `:` when too long | `after`, `always` |
|
|
175
178
|
| `lambda_body` | `hug`: the last argument hangs off the call line when it is a lambda with a block body or a broken member chain, or a creation with a broken initializer | `argument` |
|
|
176
179
|
|
|
180
|
+
`binary_wrap` also controls line breaks in `or` and `and` patterns.
|
|
181
|
+
With `before_operator_paren_own_line`, broken conditions in catch
|
|
182
|
+
filters, `if`, `while`, and `do` loops put the expression below `(` and `)`
|
|
183
|
+
on its own line, including predicate calls and mixed Boolean/pattern
|
|
184
|
+
conditions. Pattern operators take one further continuation indent.
|
|
185
|
+
Negated pattern chains and trailing Boolean operators share one continuation
|
|
186
|
+
indent, including `value is not A and not B && condition`.
|
|
187
|
+
|
|
177
188
|
```csharp
|
|
178
189
|
var invoice = invoiceService.CreateInvoice(
|
|
179
190
|
customer.Id,
|
|
@@ -220,6 +231,11 @@ orders.ForEach(order =>
|
|
|
220
231
|
| `simple_lists_inline` | `true`: lists of literals, constants, and plain names stay on one line when they fit | `false` |
|
|
221
232
|
| `trailing_commas` | `remove` | `add`, `preserve` |
|
|
222
233
|
|
|
234
|
+
Inline collection expressions keep their brackets tight, including complex
|
|
235
|
+
elements: `[new Item()]`. Broken collections put the elements on new lines.
|
|
236
|
+
When a collection starts on an argument's own line, its opening bracket
|
|
237
|
+
does not add another line break.
|
|
238
|
+
|
|
223
239
|
```csharp
|
|
224
240
|
var empty = new Order { Id = 42 };
|
|
225
241
|
|
|
@@ -369,8 +385,15 @@ lines, found by their text, in the view of the source and of the result.
|
|
|
369
385
|
| --- | --- | --- |
|
|
370
386
|
| `syntax_errors` | `skip_file`: a file that does not parse is reported and left untouched | `format_around` |
|
|
371
387
|
|
|
372
|
-
|
|
373
|
-
|
|
388
|
+
The parser supports assignments to the contextual identifier `required`
|
|
389
|
+
and C# 14 null-conditional member and element assignments. For these two
|
|
390
|
+
grammar gaps, a compatibility pass uses same-width lexical stand-ins,
|
|
391
|
+
requires a complete error-free parse, and restores the original tokens and
|
|
392
|
+
conditional-access syntax before printing and verification. Invalid input
|
|
393
|
+
is still refused; the canonical verifier is unchanged.
|
|
394
|
+
|
|
395
|
+
Other syntax newer than the grammar, such as the `#:` directives of
|
|
396
|
+
file-based apps, does not parse. With
|
|
374
397
|
`format_around`, the formatter looks for the fewest whole lines whose removal
|
|
375
398
|
lets the file parse (`isolate.py`), trying the lines around each error in
|
|
376
399
|
turn. Those lines are hidden behind placeholder comments, the rest is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prettycsharp
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
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
|
|
@@ -225,9 +225,10 @@ where any branch would change in meaning is left untouched.
|
|
|
225
225
|
|
|
226
226
|
## Code the parser cannot read
|
|
227
227
|
|
|
228
|
-
prettycsharp reads C# through tree-sitter's C# grammar,
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
prettycsharp reads C# through tree-sitter's C# grammar, with compatibility
|
|
229
|
+
support for assignments to `required` and C# 14 null-conditional assignments.
|
|
230
|
+
Other unsupported syntax, such as the `#:` directives of file-based apps,
|
|
231
|
+
does not parse. By default such a file is reported with its line and column and left
|
|
231
232
|
untouched. With `syntax_errors = "format_around"`, the formatter finds the
|
|
232
233
|
fewest lines it cannot read, leaves just those as written, formats the rest,
|
|
233
234
|
and names the lines it kept:
|
|
@@ -190,9 +190,10 @@ where any branch would change in meaning is left untouched.
|
|
|
190
190
|
|
|
191
191
|
## Code the parser cannot read
|
|
192
192
|
|
|
193
|
-
prettycsharp reads C# through tree-sitter's C# grammar,
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
prettycsharp reads C# through tree-sitter's C# grammar, with compatibility
|
|
194
|
+
support for assignments to `required` and C# 14 null-conditional assignments.
|
|
195
|
+
Other unsupported syntax, such as the `#:` directives of file-based apps,
|
|
196
|
+
does not parse. By default such a file is reported with its line and column and left
|
|
196
197
|
untouched. With `syntax_errors = "format_around"`, the formatter finds the
|
|
197
198
|
fewest lines it cannot read, leaves just those as written, formats the rest,
|
|
198
199
|
and names the lines it kept:
|
|
@@ -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.9"
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"CSharpSyntaxError",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Parsing C# source into a tree-sitter concrete syntax tree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tree_sitter_c_sharp
|
|
6
|
+
from tree_sitter import Language, Node, Parser, Tree
|
|
7
|
+
|
|
8
|
+
_LANGUAGE = Language(tree_sitter_c_sharp.language())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CSharpSyntaxError(ValueError):
|
|
12
|
+
"""The source does not parse as C#, so it is not formatted.
|
|
13
|
+
|
|
14
|
+
``line`` and ``column`` are 1-based and point at the first error the
|
|
15
|
+
parser found.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, line: int, column: int, hint: str = "") -> None:
|
|
19
|
+
super().__init__(f"syntax error at line {line}, column {column}{hint}")
|
|
20
|
+
self.line = line
|
|
21
|
+
self.column = column
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_CONDITIONAL_HINT = " (seen whole, and with its #if branches taken one view at a time)"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_tree(source: str) -> Tree:
|
|
28
|
+
"""Parse ``source``, errors and all."""
|
|
29
|
+
data = source.encode("utf-8")
|
|
30
|
+
if not data.endswith((b"\n", b"\r")):
|
|
31
|
+
# The grammar wants a line break after a directive, even at the end
|
|
32
|
+
# of the file. The extra byte comes after every node's text.
|
|
33
|
+
data += b"\n"
|
|
34
|
+
parser = Parser(_LANGUAGE)
|
|
35
|
+
tree = parser.parse(data)
|
|
36
|
+
if not tree.root_node.has_error:
|
|
37
|
+
return tree
|
|
38
|
+
return _compatibility_tree(parser, data, tree)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _compatibility_tree(parser: Parser, data: bytes, original: Tree) -> Tree:
|
|
42
|
+
"""Resolve two grammar gaps without discarding tokens or accepting errors.
|
|
43
|
+
|
|
44
|
+
Stand-ins have identical byte lengths. The syntax model reads token text
|
|
45
|
+
from the original source and restores conditional-access node shapes.
|
|
46
|
+
Only actual lexer tokens are considered, never text in comments/strings.
|
|
47
|
+
"""
|
|
48
|
+
leaves: list[Node] = []
|
|
49
|
+
stack = [original.root_node]
|
|
50
|
+
while stack:
|
|
51
|
+
node = stack.pop()
|
|
52
|
+
if node.children:
|
|
53
|
+
stack.extend(reversed(node.children))
|
|
54
|
+
elif not node.is_missing and node.type != "comment":
|
|
55
|
+
leaves.append(node)
|
|
56
|
+
modified = bytearray(data)
|
|
57
|
+
questions: set[int] = set()
|
|
58
|
+
assignment_starts = {
|
|
59
|
+
"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "??=",
|
|
60
|
+
".", "[", "(", "++", "--", "?",
|
|
61
|
+
"+", "-", "*", "/", "%", "&", "|", "^", "<", ">",
|
|
62
|
+
}
|
|
63
|
+
for node, following in zip(leaves, leaves[1:], strict=False):
|
|
64
|
+
if node.type == "required" and following.type in assignment_starts:
|
|
65
|
+
modified[node.start_byte:node.end_byte] = b"req_ired"
|
|
66
|
+
elif (
|
|
67
|
+
node.type == "?"
|
|
68
|
+
and following.type in (".", "[")
|
|
69
|
+
and node.parent is not None
|
|
70
|
+
and node.parent.type not in ("nullable_type", "conditional_expression")
|
|
71
|
+
):
|
|
72
|
+
modified[node.start_byte] = ord("!")
|
|
73
|
+
questions.add(node.start_byte)
|
|
74
|
+
if modified == data:
|
|
75
|
+
return original
|
|
76
|
+
candidate = parser.parse(bytes(modified))
|
|
77
|
+
if candidate.root_node.has_error:
|
|
78
|
+
return original
|
|
79
|
+
# Only repair null-conditional access along an assignment's receiver
|
|
80
|
+
# chain. In particular, do not turn conditional ++/-- into valid input.
|
|
81
|
+
allowed: set[int] = set()
|
|
82
|
+
stack = [candidate.root_node]
|
|
83
|
+
while stack:
|
|
84
|
+
node = stack.pop()
|
|
85
|
+
if node.type == "!" and node.start_byte in questions and _assignment_receiver(node):
|
|
86
|
+
allowed.add(node.start_byte)
|
|
87
|
+
stack.extend(node.children)
|
|
88
|
+
for position in questions - allowed:
|
|
89
|
+
modified[position] = ord("?")
|
|
90
|
+
if questions - allowed:
|
|
91
|
+
candidate = parser.parse(bytes(modified))
|
|
92
|
+
return original if candidate.root_node.has_error else candidate
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _assignment_receiver(operator: Node) -> bool:
|
|
96
|
+
receiver = operator.parent
|
|
97
|
+
if receiver is None or receiver.type != "postfix_unary_expression":
|
|
98
|
+
return False
|
|
99
|
+
while receiver.parent is not None:
|
|
100
|
+
parent = receiver.parent
|
|
101
|
+
if parent.type == "assignment_expression":
|
|
102
|
+
return parent.child_by_field_name("left") == receiver
|
|
103
|
+
field = "function" if parent.type == "invocation_expression" else "expression"
|
|
104
|
+
if parent.type not in ("member_access_expression", "element_access_expression", "invocation_expression"):
|
|
105
|
+
return False
|
|
106
|
+
if parent.child_by_field_name(field) != receiver:
|
|
107
|
+
return False
|
|
108
|
+
receiver = parent
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse(source: str) -> Tree:
|
|
113
|
+
"""Parse ``source`` and return its tree, or raise CSharpSyntaxError."""
|
|
114
|
+
tree = parse_tree(source)
|
|
115
|
+
if tree.root_node.has_error:
|
|
116
|
+
node = _first_error(tree.root_node)
|
|
117
|
+
row, column = node.start_point if node is not None else (0, 0)
|
|
118
|
+
hint = _CONDITIONAL_HINT if "#if" in source else ""
|
|
119
|
+
raise CSharpSyntaxError(row + 1, column + 1, hint)
|
|
120
|
+
return tree
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _first_error(root: Node) -> Node | None:
|
|
124
|
+
# An ERROR node often wraps a whole declaration, so follow the first
|
|
125
|
+
# erroneous child down to the innermost error, which is where the
|
|
126
|
+
# problem is. A loop, so deep nesting cannot exhaust the Python stack.
|
|
127
|
+
found = None
|
|
128
|
+
node = root
|
|
129
|
+
while True:
|
|
130
|
+
if node.is_error or node.is_missing:
|
|
131
|
+
found = node
|
|
132
|
+
child = next((c for c in node.children if c.is_error or c.is_missing or c.has_error), None)
|
|
133
|
+
if child is None:
|
|
134
|
+
return found
|
|
135
|
+
node = child
|
|
@@ -7,6 +7,7 @@ from collections.abc import Sequence
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
|
|
9
9
|
from prettycsharp.doc import (
|
|
10
|
+
ENSURE_LINE,
|
|
10
11
|
HARDLINE,
|
|
11
12
|
LINE,
|
|
12
13
|
SOFTLINE,
|
|
@@ -27,7 +28,9 @@ from prettycsharp.syntax import Node
|
|
|
27
28
|
_PRECEDENCE = {
|
|
28
29
|
"??": 1,
|
|
29
30
|
"||": 2,
|
|
31
|
+
"or": 2,
|
|
30
32
|
"&&": 3,
|
|
33
|
+
"and": 3,
|
|
31
34
|
"|": 4,
|
|
32
35
|
"^": 5,
|
|
33
36
|
"&": 6,
|
|
@@ -158,7 +161,7 @@ class Expressions(Core):
|
|
|
158
161
|
stack: list[Node] = [node]
|
|
159
162
|
while stack:
|
|
160
163
|
current = stack.pop()
|
|
161
|
-
current_op = current.child("operator") if current.type ==
|
|
164
|
+
current_op = current.child("operator") if current.type == node.type else None
|
|
162
165
|
if (
|
|
163
166
|
current_op is not None
|
|
164
167
|
and _PRECEDENCE.get(current_op.text or "", -1) == level
|
|
@@ -676,7 +679,10 @@ class Expressions(Core):
|
|
|
676
679
|
return group([self.tok(open_tok), indent([SOFTLINE, body]), self.closing(close_tok, SOFTLINE)])
|
|
677
680
|
force = self._forced_initializer([[_element_value(i[0])] for i in items if len(i) == 1])
|
|
678
681
|
return group(
|
|
679
|
-
[
|
|
682
|
+
[
|
|
683
|
+
IfBreak(ENSURE_LINE, ""), self.tok(open_tok),
|
|
684
|
+
indent([SOFTLINE, join(LINE, docs)]), self.closing(close_tok, SOFTLINE),
|
|
685
|
+
],
|
|
680
686
|
should_break=force,
|
|
681
687
|
)
|
|
682
688
|
|
|
@@ -868,8 +874,16 @@ class Expressions(Core):
|
|
|
868
874
|
def p_is_expression(self, node: Node) -> Doc:
|
|
869
875
|
return self.spaced(node.children)
|
|
870
876
|
|
|
871
|
-
def p_is_pattern_expression(self, node: Node) -> Doc:
|
|
872
|
-
|
|
877
|
+
def p_is_pattern_expression(self, node: Node, *, in_condition: bool = False) -> Doc:
|
|
878
|
+
pattern = node.child("pattern")
|
|
879
|
+
if not in_condition or pattern is None or pattern.verbatim or pattern.type not in (
|
|
880
|
+
"and_pattern", "or_pattern", "negated_pattern"
|
|
881
|
+
):
|
|
882
|
+
return self.spaced(node.children)
|
|
883
|
+
return join(" ", [
|
|
884
|
+
self.logical_pattern(child, group_result=False) if child is pattern else self.print(child)
|
|
885
|
+
for child in node.children
|
|
886
|
+
])
|
|
873
887
|
|
|
874
888
|
def p_range_expression(self, node: Node) -> Doc:
|
|
875
889
|
return self.join_tokens([(c, self.print(c)) for c in node.children])
|
|
@@ -897,16 +911,19 @@ class Expressions(Core):
|
|
|
897
911
|
|
|
898
912
|
# Patterns ------------------------------------------------------------
|
|
899
913
|
|
|
900
|
-
def p_property_pattern_clause(self, node: Node) -> Doc:
|
|
914
|
+
def p_property_pattern_clause(self, node: Node, *, leading_space: bool = False) -> Doc:
|
|
901
915
|
if len(node.children) < 2:
|
|
902
916
|
return self.generic(node)
|
|
903
917
|
open_tok = node.children[0]
|
|
904
918
|
close_tok = node.children[-1]
|
|
905
919
|
items, commas = self.items_and_commas(node)
|
|
920
|
+
before: Doc = ""
|
|
921
|
+
if leading_space:
|
|
922
|
+
before = LINE if self.settings.brace_style == "allman" else " "
|
|
906
923
|
if not items:
|
|
907
|
-
return group([self.tok(open_tok), self.closing(close_tok, LINE)])
|
|
924
|
+
return group([before, self.tok(open_tok), self.closing(close_tok, LINE)])
|
|
908
925
|
docs = self._comma_docs(items, commas)
|
|
909
|
-
return group([self.tok(open_tok), indent([LINE, join(LINE, docs)]), self.closing(close_tok, LINE)])
|
|
926
|
+
return group([before, self.tok(open_tok), indent([LINE, join(LINE, docs)]), self.closing(close_tok, LINE)])
|
|
910
927
|
|
|
911
928
|
def p_positional_pattern_clause(self, node: Node) -> Doc:
|
|
912
929
|
return self.arguments(node)
|
|
@@ -936,13 +953,49 @@ class Expressions(Core):
|
|
|
936
953
|
return self.spaced(node.children)
|
|
937
954
|
|
|
938
955
|
def p_and_pattern(self, node: Node) -> Doc:
|
|
939
|
-
return self.
|
|
956
|
+
return self.logical_pattern(node)
|
|
940
957
|
|
|
941
958
|
def p_or_pattern(self, node: Node) -> Doc:
|
|
942
|
-
return self.
|
|
959
|
+
return self.logical_pattern(node)
|
|
943
960
|
|
|
944
961
|
def p_negated_pattern(self, node: Node) -> Doc:
|
|
945
|
-
return self.
|
|
962
|
+
return self.logical_pattern(node)
|
|
963
|
+
|
|
964
|
+
def logical_pattern(self, node: Node, *, group_result: bool = True) -> Doc:
|
|
965
|
+
# The grammar nests `not A and not B` under successive negations,
|
|
966
|
+
# sometimes including trailing &&/|| expressions in constant patterns.
|
|
967
|
+
# Share a continuation level without moving any tokens or parentheses.
|
|
968
|
+
segments: list[list[Node]] = [[]]
|
|
969
|
+
operators: list[Node] = []
|
|
970
|
+
stack = [node]
|
|
971
|
+
while stack:
|
|
972
|
+
current = stack.pop()
|
|
973
|
+
if current.type == "constant_pattern" and len(current.children) == 1 and not current.verbatim:
|
|
974
|
+
value = current.children[0]
|
|
975
|
+
value_op = value.child("operator")
|
|
976
|
+
if value.type == "binary_expression" and value_op is not None and value_op.text in ("&&", "||"):
|
|
977
|
+
stack.append(value)
|
|
978
|
+
continue
|
|
979
|
+
op = current.child("operator")
|
|
980
|
+
logical = current.type in ("and_pattern", "or_pattern", "negated_pattern") or (
|
|
981
|
+
current.type == "binary_expression" and op is not None and op.text in ("&&", "||")
|
|
982
|
+
)
|
|
983
|
+
if logical and not current.verbatim:
|
|
984
|
+
stack.extend(reversed(current.children))
|
|
985
|
+
elif not current.named and current.text in ("and", "or", "&&", "||"):
|
|
986
|
+
operators.append(current)
|
|
987
|
+
segments.append([])
|
|
988
|
+
else:
|
|
989
|
+
segments[-1].append(current)
|
|
990
|
+
first = self.spaced(segments[0])
|
|
991
|
+
rest: list[Doc] = []
|
|
992
|
+
for operator, segment in zip(operators, segments[1:], strict=True):
|
|
993
|
+
if self.settings.binary_wrap == "after_operator":
|
|
994
|
+
rest.append([" ", self.tok(operator), LINE, self.spaced(segment)])
|
|
995
|
+
else:
|
|
996
|
+
rest.append([LINE, self.tok(operator), " ", self.spaced(segment)])
|
|
997
|
+
contents: Doc = [first, indent(rest)]
|
|
998
|
+
return group(contents) if group_result else contents
|
|
946
999
|
|
|
947
1000
|
def p_declaration_pattern(self, node: Node) -> Doc:
|
|
948
1001
|
return self.spaced(node.children)
|
|
@@ -951,6 +1004,10 @@ class Expressions(Core):
|
|
|
951
1004
|
parts: list[Doc] = []
|
|
952
1005
|
previous: Node | None = None
|
|
953
1006
|
for child in node.children:
|
|
1007
|
+
if previous is not None and child.type == "property_pattern_clause" and not child.verbatim:
|
|
1008
|
+
parts.append(self.p_property_pattern_clause(child, leading_space=True))
|
|
1009
|
+
previous = child
|
|
1010
|
+
continue
|
|
954
1011
|
if previous is not None and not (child.type == "positional_pattern_clause" and previous.named):
|
|
955
1012
|
parts.append(" ")
|
|
956
1013
|
parts.append(self.print(child))
|
|
@@ -144,13 +144,18 @@ class Statements(Expressions):
|
|
|
144
144
|
def condition(self, open_tok: Node, condition: Node, close_tok: Node) -> Doc:
|
|
145
145
|
if (
|
|
146
146
|
self.settings.binary_wrap == "before_operator_paren_own_line"
|
|
147
|
-
and condition.type == "binary_expression"
|
|
148
147
|
and not condition.verbatim
|
|
149
148
|
):
|
|
149
|
+
if condition.type == "binary_expression":
|
|
150
|
+
contents = self.binary(condition, in_condition=True)
|
|
151
|
+
elif condition.type == "is_pattern_expression":
|
|
152
|
+
contents = self.p_is_pattern_expression(condition, in_condition=True)
|
|
153
|
+
else:
|
|
154
|
+
contents = self.print(condition)
|
|
150
155
|
return group(
|
|
151
156
|
[
|
|
152
157
|
self.tok(open_tok),
|
|
153
|
-
indent([SOFTLINE,
|
|
158
|
+
indent([SOFTLINE, contents]),
|
|
154
159
|
SOFTLINE,
|
|
155
160
|
self.tok(close_tok),
|
|
156
161
|
]
|
|
@@ -226,11 +226,55 @@ def _copy(tree: Tree, data: bytes) -> tuple[Node, list[Node], list[Comment]]:
|
|
|
226
226
|
node.children.append(child)
|
|
227
227
|
pending.append((ts_child, child))
|
|
228
228
|
stack.extend(reversed(pending))
|
|
229
|
+
_restore_conditional_assignments(root)
|
|
229
230
|
tokens.extend(root.tokens())
|
|
230
231
|
comments.sort(key=lambda c: c.start)
|
|
231
232
|
return root, tokens, comments
|
|
232
233
|
|
|
233
234
|
|
|
235
|
+
def _restore_conditional_assignments(root: Node) -> None:
|
|
236
|
+
"""Restore conditional accesses parsed with same-width ``!`` stand-ins."""
|
|
237
|
+
nodes: list[Node] = []
|
|
238
|
+
stack = [root]
|
|
239
|
+
while stack:
|
|
240
|
+
node = stack.pop()
|
|
241
|
+
nodes.append(node)
|
|
242
|
+
stack.extend(node.children)
|
|
243
|
+
for node in reversed(nodes):
|
|
244
|
+
if node.type not in ("member_access_expression", "element_access_expression") or not node.children:
|
|
245
|
+
continue
|
|
246
|
+
receiver = node.children[0]
|
|
247
|
+
if receiver.type != "postfix_unary_expression" or len(receiver.children) != 2:
|
|
248
|
+
continue
|
|
249
|
+
condition, question = receiver.children
|
|
250
|
+
if question.type != "!" or question.text != "?":
|
|
251
|
+
continue
|
|
252
|
+
children = node.children[1:]
|
|
253
|
+
if node.type == "element_access_expression":
|
|
254
|
+
if len(children) != 1 or children[0].type != "bracketed_argument_list":
|
|
255
|
+
continue
|
|
256
|
+
children = children[0].children
|
|
257
|
+
binding = Node(
|
|
258
|
+
type="member_binding_expression" if node.type == "member_access_expression" else "element_binding_expression",
|
|
259
|
+
named=True,
|
|
260
|
+
start=children[0].start,
|
|
261
|
+
end=node.end,
|
|
262
|
+
line=children[0].line,
|
|
263
|
+
column=children[0].column,
|
|
264
|
+
end_line=node.end_line,
|
|
265
|
+
children=children,
|
|
266
|
+
parent=node,
|
|
267
|
+
)
|
|
268
|
+
for child in children:
|
|
269
|
+
child.parent = binding
|
|
270
|
+
condition.field = "condition"
|
|
271
|
+
condition.parent = node
|
|
272
|
+
question.type = "?"
|
|
273
|
+
question.parent = node
|
|
274
|
+
node.type = "conditional_access_expression"
|
|
275
|
+
node.children = [condition, question, binding]
|
|
276
|
+
|
|
277
|
+
|
|
234
278
|
def _new_node(ts_node: TSNode, field_name: str | None, data: bytes) -> Node:
|
|
235
279
|
text = None
|
|
236
280
|
end = ts_node.end_byte
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Assignments
|
|
2
|
+
{
|
|
3
|
+
public required string Name { get; init; }
|
|
4
|
+
void Update(dynamic target, int[] items, int index)
|
|
5
|
+
{
|
|
6
|
+
long required = 0;
|
|
7
|
+
required = Math.Max(required, index + 1);
|
|
8
|
+
required -= index;
|
|
9
|
+
target?.Route = required;
|
|
10
|
+
items?[index] += 1;
|
|
11
|
+
target?.Child?.Route = target?.Route;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -10,12 +10,12 @@ from prettycsharp.isolate import isolate
|
|
|
10
10
|
|
|
11
11
|
AROUND = Settings().with_values({"syntax_errors": "format_around"})
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
UNREADABLE_SOURCE = """class C
|
|
14
14
|
{
|
|
15
15
|
void M()
|
|
16
16
|
{
|
|
17
17
|
Run();
|
|
18
|
-
a?.B =
|
|
18
|
+
a?.B = ;
|
|
19
19
|
int x=1;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -26,13 +26,13 @@ NEW_SYNTAX = """class C
|
|
|
26
26
|
|
|
27
27
|
def test_skip_file_is_the_default() -> None:
|
|
28
28
|
with pytest.raises(CSharpSyntaxError):
|
|
29
|
-
format_source(
|
|
29
|
+
format_source(UNREADABLE_SOURCE)
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def test_unreadable_line_is_kept_and_the_rest_formatted() -> None:
|
|
33
|
-
result = format_details(
|
|
33
|
+
result = format_details(UNREADABLE_SOURCE, AROUND)
|
|
34
34
|
assert result.text == (
|
|
35
|
-
"class C\n{\n void M()\n {\n Run();\n a?.B =
|
|
35
|
+
"class C\n{\n void M()\n {\n Run();\n a?.B = ;\n int x = 1;\n }\n\n"
|
|
36
36
|
" void N() => Go();\n}\n"
|
|
37
37
|
)
|
|
38
38
|
assert result.kept_lines == (6,)
|
|
@@ -40,9 +40,9 @@ def test_unreadable_line_is_kept_and_the_rest_formatted() -> None:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def test_isolate_hides_the_fewest_lines() -> None:
|
|
43
|
-
masked = isolate(
|
|
43
|
+
masked = isolate(UNREADABLE_SOURCE)
|
|
44
44
|
assert masked is not None
|
|
45
|
-
assert list(masked.hidden.values()) == [" a?.B =
|
|
45
|
+
assert list(masked.hidden.values()) == [" a?.B = ;"]
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_file_scoped_directives_of_file_based_apps() -> None:
|
|
@@ -63,7 +63,7 @@ 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
|
-
|
|
66
|
+
SPLIT_AND_UNREADABLE = """#if NET
|
|
67
67
|
public sealed class Pool : IAsyncDisposable
|
|
68
68
|
#else
|
|
69
69
|
public sealed class Pool : IDisposable
|
|
@@ -72,7 +72,7 @@ public sealed class Pool : IDisposable
|
|
|
72
72
|
static public int Size=3;
|
|
73
73
|
void M()
|
|
74
74
|
{
|
|
75
|
-
a?.B =
|
|
75
|
+
a?.B = ;
|
|
76
76
|
Run( );
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -80,31 +80,31 @@ public sealed class Pool : IDisposable
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def test_format_around_works_inside_each_if_view() -> None:
|
|
83
|
-
result = format_details(
|
|
83
|
+
result = format_details(SPLIT_AND_UNREADABLE, AROUND)
|
|
84
84
|
assert result.text == (
|
|
85
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 =
|
|
86
|
+
"{\n public static int Size = 3;\n\n void M()\n {\n a?.B = ;\n Run();\n }\n}\n"
|
|
87
87
|
)
|
|
88
88
|
assert result.kept_lines == (10,)
|
|
89
89
|
assert format_source(result.text, AROUND) == result.text
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def test_split_and_unreadable_syntax_is_still_skipped_by_default() -> None:
|
|
93
93
|
with pytest.raises(CSharpSyntaxError):
|
|
94
|
-
format_source(
|
|
94
|
+
format_source(SPLIT_AND_UNREADABLE)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def
|
|
98
|
-
source =
|
|
97
|
+
def test_one_line_member_with_unreadable_syntax_keeps_only_that_line() -> None:
|
|
98
|
+
source = SPLIT_AND_UNREADABLE.replace(" void M()\n {\n a?.B = ;\n Run( );\n }\n", " void M() { a?.B = ; Run( ); }\n")
|
|
99
99
|
result = format_details(source, AROUND)
|
|
100
100
|
assert " public static int Size = 3;" in result.text
|
|
101
|
-
assert " void M() { a?.B =
|
|
101
|
+
assert " void M() { a?.B = ; Run( ); }" in result.text
|
|
102
102
|
assert result.kept_lines == (8,)
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def test_command_line_reports_the_kept_lines(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
106
106
|
path = tmp_path / "C.cs"
|
|
107
|
-
path.write_text(
|
|
107
|
+
path.write_text(UNREADABLE_SOURCE)
|
|
108
108
|
assert main(["format", str(path), "-o", "syntax_errors=format_around"]) == 0
|
|
109
109
|
assert "line 6 left as written" in capsys.readouterr().err
|
|
110
110
|
assert "int x = 1;" in path.read_text()
|