prettycsharp 1.1.7__tar.gz → 1.1.8__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.8}/CHANGELOG.md +13 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/DESIGN.md +14 -2
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/PKG-INFO +5 -4
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/README.md +4 -3
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/__init__.py +1 -1
- prettycsharp-1.1.8/src/prettycsharp/parsing.py +135 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/printer/expressions.py +15 -6
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/syntax.py +44 -0
- prettycsharp-1.1.8/tests/fuzz_corpus/csharp/conditional_assignments.cs +13 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_format_around.py +17 -17
- prettycsharp-1.1.8/tests/test_grammar_compatibility.py +102 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_layout.py +46 -0
- prettycsharp-1.1.7/src/prettycsharp/parsing.py +0 -60
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.gitattributes +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/codeql/codeql-config.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/dependabot.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/build.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/build.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/fuzz.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/fuzz.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/pip-audit.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/pip-audit.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/runtime.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/runtime.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/test.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/test.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/zizmor.in +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/requirements/zizmor.txt +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/accepted.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/clamav/Dockerfile +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/malware-accepted.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/semgrep/Dockerfile +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/yara.json +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/security/yara_update.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/auto-merge.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/ci.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/fuzz.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/malware-scan.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/publish.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/scorecard.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/security.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/workflows/update-yara-rules.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.github/zizmor.yml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/.gitignore +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/AGENTS.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/CLAUDE.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/LICENSE +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/SECURITY.md +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/fuzz/fuzz_formatter.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/pyproject.toml +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/scripts/security/malware_report.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/scripts/security/security_report.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/__main__.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/cli.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/conditional.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/config.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/doc.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/formatter.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/ignore.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/isolate.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/printer/__init__.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/printer/core.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/printer/declarations.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/printer/statements.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/project.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/py.typed +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/rewrites.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/settings.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/src/prettycsharp/verify.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/comment_only_accessor_list.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/declarations.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/directives.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/embedded_statements.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/expressions.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/members.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/order.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/queries_and_patterns.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/split_by_conditionals.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/statements.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/strings.cs +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/support.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_cli.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_conditional.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_config.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_doc.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_docs.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_fuzz_corpus.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_ignore.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_parsing.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_project.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_rewrites.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_settings.py +0 -0
- {prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/test_verify.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Each release's section becomes the body of its GitHub release.
|
|
4
4
|
|
|
5
|
+
## [1.1.8] - 2026-10-18
|
|
6
|
+
|
|
7
|
+
- Long `or` and `and` patterns wrap at the configured line width using
|
|
8
|
+
`binary_wrap`, while short patterns stay inline.
|
|
9
|
+
- A multiline property pattern moves its opening brace onto its own line
|
|
10
|
+
with Allman braces, including patterns in catch filters, avoiding IDE0055.
|
|
11
|
+
- Assignments to a variable named `required`, including compound assignments,
|
|
12
|
+
format without mistaking the identifier for a member modifier.
|
|
13
|
+
- C# 14 null-conditional member and element assignments, such as
|
|
14
|
+
`telemetry?.Route = value` and `items?[index] += value`, format instead of
|
|
15
|
+
being refused. Original tokens, comments, and conditional-access semantics
|
|
16
|
+
are preserved, and malformed input is still refused.
|
|
17
|
+
|
|
5
18
|
## [1.1.7] - 2026-10-18
|
|
6
19
|
|
|
7
20
|
- 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,8 @@ 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
|
+
|
|
177
182
|
```csharp
|
|
178
183
|
var invoice = invoiceService.CreateInvoice(
|
|
179
184
|
customer.Id,
|
|
@@ -369,8 +374,15 @@ lines, found by their text, in the view of the source and of the result.
|
|
|
369
374
|
| --- | --- | --- |
|
|
370
375
|
| `syntax_errors` | `skip_file`: a file that does not parse is reported and left untouched | `format_around` |
|
|
371
376
|
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
The parser supports assignments to the contextual identifier `required`
|
|
378
|
+
and C# 14 null-conditional member and element assignments. For these two
|
|
379
|
+
grammar gaps, a compatibility pass uses same-width lexical stand-ins,
|
|
380
|
+
requires a complete error-free parse, and restores the original tokens and
|
|
381
|
+
conditional-access syntax before printing and verification. Invalid input
|
|
382
|
+
is still refused; the canonical verifier is unchanged.
|
|
383
|
+
|
|
384
|
+
Other syntax newer than the grammar, such as the `#:` directives of
|
|
385
|
+
file-based apps, does not parse. With
|
|
374
386
|
`format_around`, the formatter looks for the fewest whole lines whose removal
|
|
375
387
|
lets the file parse (`isolate.py`), trying the lines around each error in
|
|
376
388
|
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.8
|
|
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.8"
|
|
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
|
|
@@ -27,7 +27,9 @@ from prettycsharp.syntax import Node
|
|
|
27
27
|
_PRECEDENCE = {
|
|
28
28
|
"??": 1,
|
|
29
29
|
"||": 2,
|
|
30
|
+
"or": 2,
|
|
30
31
|
"&&": 3,
|
|
32
|
+
"and": 3,
|
|
31
33
|
"|": 4,
|
|
32
34
|
"^": 5,
|
|
33
35
|
"&": 6,
|
|
@@ -158,7 +160,7 @@ class Expressions(Core):
|
|
|
158
160
|
stack: list[Node] = [node]
|
|
159
161
|
while stack:
|
|
160
162
|
current = stack.pop()
|
|
161
|
-
current_op = current.child("operator") if current.type ==
|
|
163
|
+
current_op = current.child("operator") if current.type == node.type else None
|
|
162
164
|
if (
|
|
163
165
|
current_op is not None
|
|
164
166
|
and _PRECEDENCE.get(current_op.text or "", -1) == level
|
|
@@ -897,16 +899,19 @@ class Expressions(Core):
|
|
|
897
899
|
|
|
898
900
|
# Patterns ------------------------------------------------------------
|
|
899
901
|
|
|
900
|
-
def p_property_pattern_clause(self, node: Node) -> Doc:
|
|
902
|
+
def p_property_pattern_clause(self, node: Node, *, leading_space: bool = False) -> Doc:
|
|
901
903
|
if len(node.children) < 2:
|
|
902
904
|
return self.generic(node)
|
|
903
905
|
open_tok = node.children[0]
|
|
904
906
|
close_tok = node.children[-1]
|
|
905
907
|
items, commas = self.items_and_commas(node)
|
|
908
|
+
before: Doc = ""
|
|
909
|
+
if leading_space:
|
|
910
|
+
before = LINE if self.settings.brace_style == "allman" else " "
|
|
906
911
|
if not items:
|
|
907
|
-
return group([self.tok(open_tok), self.closing(close_tok, LINE)])
|
|
912
|
+
return group([before, self.tok(open_tok), self.closing(close_tok, LINE)])
|
|
908
913
|
docs = self._comma_docs(items, commas)
|
|
909
|
-
return group([self.tok(open_tok), indent([LINE, join(LINE, docs)]), self.closing(close_tok, LINE)])
|
|
914
|
+
return group([before, self.tok(open_tok), indent([LINE, join(LINE, docs)]), self.closing(close_tok, LINE)])
|
|
910
915
|
|
|
911
916
|
def p_positional_pattern_clause(self, node: Node) -> Doc:
|
|
912
917
|
return self.arguments(node)
|
|
@@ -936,10 +941,10 @@ class Expressions(Core):
|
|
|
936
941
|
return self.spaced(node.children)
|
|
937
942
|
|
|
938
943
|
def p_and_pattern(self, node: Node) -> Doc:
|
|
939
|
-
return self.
|
|
944
|
+
return self.binary(node, in_condition=False)
|
|
940
945
|
|
|
941
946
|
def p_or_pattern(self, node: Node) -> Doc:
|
|
942
|
-
return self.
|
|
947
|
+
return self.binary(node, in_condition=False)
|
|
943
948
|
|
|
944
949
|
def p_negated_pattern(self, node: Node) -> Doc:
|
|
945
950
|
return self.spaced(node.children)
|
|
@@ -951,6 +956,10 @@ class Expressions(Core):
|
|
|
951
956
|
parts: list[Doc] = []
|
|
952
957
|
previous: Node | None = None
|
|
953
958
|
for child in node.children:
|
|
959
|
+
if previous is not None and child.type == "property_pattern_clause" and not child.verbatim:
|
|
960
|
+
parts.append(self.p_property_pattern_clause(child, leading_space=True))
|
|
961
|
+
previous = child
|
|
962
|
+
continue
|
|
954
963
|
if previous is not None and not (child.type == "positional_pattern_clause" and previous.named):
|
|
955
964
|
parts.append(" ")
|
|
956
965
|
parts.append(self.print(child))
|
|
@@ -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()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Grammar gaps must not cause valid assignments to be skipped or changed."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from prettycsharp import CSharpSyntaxError, Settings, format_details
|
|
6
|
+
from prettycsharp.syntax import Node, build
|
|
7
|
+
from prettycsharp.verify import canonical
|
|
8
|
+
|
|
9
|
+
from support import check, fmt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("operator", ["=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "??="])
|
|
13
|
+
def test_required_assignment(operator: str) -> None:
|
|
14
|
+
check(f"required{operator}value;", f"required {operator} value;\n")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize("target", ["a?.B", "a?[i]", "a?.B?.C", "a?.B?[i]", "a?[i]?.B", "a?.Get().B"])
|
|
18
|
+
@pytest.mark.parametrize("operator", ["=", "+=", "-=", "??="])
|
|
19
|
+
def test_null_conditional_assignment(target: str, operator: str) -> None:
|
|
20
|
+
check(f"{target}{operator}value;", f"{target} {operator} value;\n")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_reported_required_assignments_in_a_loop() -> None:
|
|
24
|
+
check(
|
|
25
|
+
"long required=0;foreach(var index in indices){required=Math.Max(required,index+1);required-=range;}",
|
|
26
|
+
"long required = 0;\n\nforeach (var index in indices)\n{\n"
|
|
27
|
+
" required = Math.Max(required, index + 1);\n required -= range;\n}\n",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_comments_literals_and_original_token_text_are_preserved() -> None:
|
|
32
|
+
source = 'required /* keep */ = Get("required = x; a?.B = x;");\na?./* receiver */B = required;'
|
|
33
|
+
output = fmt(source)
|
|
34
|
+
assert '"required = x; a?.B = x;"' in output
|
|
35
|
+
assert 'required /* keep */ = ' in output
|
|
36
|
+
assert 'a?. /* receiver */B = required;' in output
|
|
37
|
+
assert "req_ired" not in output
|
|
38
|
+
assert fmt(output) == output
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_required_member_modifier_is_preserved() -> None:
|
|
42
|
+
check(
|
|
43
|
+
"class C{public required string Name{get;init;}}",
|
|
44
|
+
"class C\n{\n public required string Name { get; init; }\n}\n",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize("source", ["required = ;", "required -= ;", "a?.B = ;", "a?[i] += ;", "a?. = 1;", "a?[] = 1;", "class { required = value; }", "a?.B = 1; class {"])
|
|
49
|
+
def test_invalid_input_remains_refused(source: str) -> None:
|
|
50
|
+
with pytest.raises(CSharpSyntaxError):
|
|
51
|
+
fmt(source)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize(
|
|
55
|
+
("before", "after"),
|
|
56
|
+
[
|
|
57
|
+
("a?.B = 1;", "a.B = 1;"),
|
|
58
|
+
("a?.B = 1;", "a!.B = 1;"),
|
|
59
|
+
("a?[i] = 1;", "a[i] = 1;"),
|
|
60
|
+
("a?.B += 1;", "a?.B = 1;"),
|
|
61
|
+
("required = 1;", "req_ired = 1;"),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
def test_verifier_distinguishes_semantic_changes(before: str, after: str) -> None:
|
|
65
|
+
assert canonical(build(before).root) != canonical(build(after).root)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_supported_syntax_needs_no_format_around_exclusions() -> None:
|
|
69
|
+
result = format_details("required=1;a?.B=required;", Settings(syntax_errors="format_around"))
|
|
70
|
+
assert result.text == "required = 1;\na?.B = required;\n"
|
|
71
|
+
assert result.kept_lines == ()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_non_ascii_and_crlf_positions_are_preserved() -> None:
|
|
75
|
+
source = 'var label="' + chr(0xE9) + '";\r\nrequired=1;\r\na?.B=required;\r\n'
|
|
76
|
+
output = fmt(source)
|
|
77
|
+
assert output == 'var label = "' + chr(0xE9) + '";\r\nrequired = 1;\r\na?.B = required;\r\n'
|
|
78
|
+
assert fmt(output) == output
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_nullable_types_and_ternary_collections_are_not_conditional_accesses() -> None:
|
|
82
|
+
check(
|
|
83
|
+
"required=1;string?[] values=ok?[1]:[2];a?.B=new string?[0];",
|
|
84
|
+
"required = 1;\nstring?[] values = ok ? [1] : [2];\na?.B = new string?[0];\n",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("access", ["a?.B", "a?[i]"])
|
|
89
|
+
def test_assignment_target_has_the_native_conditional_access_shape(access: str) -> None:
|
|
90
|
+
assigned = build(f"{access} = value;")
|
|
91
|
+
read = build(f"var value = {access};")
|
|
92
|
+
# Compare actual syntax subtrees, without changing canonicalization.
|
|
93
|
+
def conditional(node: Node) -> Node:
|
|
94
|
+
stack = [node]
|
|
95
|
+
while stack:
|
|
96
|
+
current = stack.pop()
|
|
97
|
+
if current.type == "conditional_access_expression":
|
|
98
|
+
return current
|
|
99
|
+
stack.extend(current.children)
|
|
100
|
+
raise LookupError
|
|
101
|
+
|
|
102
|
+
assert canonical(conditional(assigned.root)) == canonical(conditional(read.root))
|
|
@@ -108,6 +108,52 @@ def test_four_space_indent() -> None:
|
|
|
108
108
|
assert fmt("class A{void M(){Run();Stop();}}") == "class A\n{\n void M()\n {\n Run();\n Stop();\n }\n}\n"
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def test_catch_filter_wraps_logical_pattern_before_expanding_property_pattern() -> None:
|
|
112
|
+
check(
|
|
113
|
+
'try { Run(); } catch (Exception error) when (error is ObjectDisposedException or SocketException '
|
|
114
|
+
'or ArgumentException { ParamName: "socketAddress" }) { Stop(); }',
|
|
115
|
+
'try\n{\n Run();\n}\n\n'
|
|
116
|
+
'catch (Exception error) when (error is ObjectDisposedException\n'
|
|
117
|
+
' or SocketException\n or ArgumentException { ParamName: "socketAddress" })\n{\n Stop();\n}\n',
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize("style", ["allman", "kr", "mixed"])
|
|
122
|
+
def test_broken_property_pattern_respects_brace_style(style: str) -> None:
|
|
123
|
+
separator = "\n" if style == "allman" else " "
|
|
124
|
+
check(
|
|
125
|
+
'var match = value is SomeType { VeryLongProperty: "a rather long value" };',
|
|
126
|
+
f'var match = value is SomeType{separator}'
|
|
127
|
+
'{\n VeryLongProperty: "a rather long value"\n};\n',
|
|
128
|
+
max_line_length=45,
|
|
129
|
+
brace_style=style,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_short_property_pattern_remains_inline() -> None:
|
|
134
|
+
check('var match=value is Type{Id:1};', 'var match = value is Type { Id: 1 };\n')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.parametrize("operator", ["or", "and"])
|
|
138
|
+
@pytest.mark.parametrize("wrap", ["before_operator_paren_own_line", "before_operator", "after_operator"])
|
|
139
|
+
def test_long_logical_patterns_wrap(operator: str, wrap: str) -> None:
|
|
140
|
+
if wrap == "after_operator":
|
|
141
|
+
expected = f"var ok = value is LongPatternOne {operator}\n LongPatternTwo {operator}\n LongPatternThree;\n"
|
|
142
|
+
else:
|
|
143
|
+
expected = f"var ok = value is LongPatternOne\n {operator} LongPatternTwo\n {operator} LongPatternThree;\n"
|
|
144
|
+
check(
|
|
145
|
+
f"var ok = value is LongPatternOne {operator} LongPatternTwo {operator} LongPatternThree;",
|
|
146
|
+
expected,
|
|
147
|
+
max_line_length=50,
|
|
148
|
+
binary_wrap=wrap,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize("pattern", ["A or B", "A and B", "A or B and C", "(A or B) and C"])
|
|
153
|
+
def test_short_logical_patterns_keep_grouping_and_stay_inline(pattern: str) -> None:
|
|
154
|
+
check(f"var ok=value is {pattern};", f"var ok = value is {pattern};\n")
|
|
155
|
+
|
|
156
|
+
|
|
111
157
|
def test_call_breaks_one_argument_per_line_with_paren_on_its_own_line() -> None:
|
|
112
158
|
check(
|
|
113
159
|
"""
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
return Parser(_LANGUAGE).parse(data)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def parse(source: str) -> Tree:
|
|
38
|
-
"""Parse ``source`` and return its tree, or raise CSharpSyntaxError."""
|
|
39
|
-
tree = parse_tree(source)
|
|
40
|
-
if tree.root_node.has_error:
|
|
41
|
-
node = _first_error(tree.root_node)
|
|
42
|
-
row, column = node.start_point if node is not None else (0, 0)
|
|
43
|
-
hint = _CONDITIONAL_HINT if "#if" in source else ""
|
|
44
|
-
raise CSharpSyntaxError(row + 1, column + 1, hint)
|
|
45
|
-
return tree
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _first_error(root: Node) -> Node | None:
|
|
49
|
-
# An ERROR node often wraps a whole declaration, so follow the first
|
|
50
|
-
# erroneous child down to the innermost error, which is where the
|
|
51
|
-
# problem is. A loop, so deep nesting cannot exhaust the Python stack.
|
|
52
|
-
found = None
|
|
53
|
-
node = root
|
|
54
|
-
while True:
|
|
55
|
-
if node.is_error or node.is_missing:
|
|
56
|
-
found = node
|
|
57
|
-
child = next((c for c in node.children if c.is_error or c.is_missing or c.has_error), None)
|
|
58
|
-
if child is None:
|
|
59
|
-
return found
|
|
60
|
-
node = child
|
|
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
|
{prettycsharp-1.1.7 → prettycsharp-1.1.8}/tests/fuzz_corpus/csharp/comment_only_accessor_list.cs
RENAMED
|
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
|