prettycsharp 1.0.0__py3-none-any.whl
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/__init__.py +9 -0
- prettycsharp/__main__.py +5 -0
- prettycsharp/cli.py +250 -0
- prettycsharp/config.py +263 -0
- prettycsharp/doc.py +537 -0
- prettycsharp/formatter.py +106 -0
- prettycsharp/ignore.py +184 -0
- prettycsharp/parsing.py +53 -0
- prettycsharp/printer/__init__.py +28 -0
- prettycsharp/printer/core.py +540 -0
- prettycsharp/printer/declarations.py +623 -0
- prettycsharp/printer/expressions.py +1107 -0
- prettycsharp/printer/statements.py +603 -0
- prettycsharp/project.py +214 -0
- prettycsharp/py.typed +0 -0
- prettycsharp/rewrites.py +732 -0
- prettycsharp/settings.py +192 -0
- prettycsharp/syntax.py +367 -0
- prettycsharp/verify.py +312 -0
- prettycsharp-1.0.0.dist-info/METADATA +212 -0
- prettycsharp-1.0.0.dist-info/RECORD +24 -0
- prettycsharp-1.0.0.dist-info/WHEEL +4 -0
- prettycsharp-1.0.0.dist-info/entry_points.txt +2 -0
- prettycsharp-1.0.0.dist-info/licenses/LICENSE +21 -0
prettycsharp/verify.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Checking that formatting changed layout, never meaning.
|
|
2
|
+
|
|
3
|
+
The input and the output are parsed and reduced to canonical syntax trees,
|
|
4
|
+
which must be equal. Canonicalization removes the differences the rewrites
|
|
5
|
+
are allowed to make, each in its simplest form: every parenthesized
|
|
6
|
+
expression is replaced by what it holds (the tree keeps the grouping), the
|
|
7
|
+
modifiers of a declaration and a run of using directives are sorted, an
|
|
8
|
+
attribute list is split into one list per attribute, a trailing comma is
|
|
9
|
+
dropped, a single-statement body is put in expression form, a namespace is
|
|
10
|
+
made file-scoped, an embedded statement gets braces, and regions are
|
|
11
|
+
dropped. Comments must hold the same words, counted as a multiset, since
|
|
12
|
+
rewrapping and reordering move them.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
20
|
+
from prettycsharp.syntax import Comment, Node, SourceFile
|
|
21
|
+
|
|
22
|
+
Canon = tuple[object, ...]
|
|
23
|
+
|
|
24
|
+
_EMBEDDED = {
|
|
25
|
+
"if_statement": ("consequence", "alternative"),
|
|
26
|
+
"while_statement": ("body",),
|
|
27
|
+
"for_statement": ("body",),
|
|
28
|
+
"foreach_statement": ("body",),
|
|
29
|
+
"do_statement": ("body",),
|
|
30
|
+
"using_statement": ("body",),
|
|
31
|
+
"lock_statement": ("body",),
|
|
32
|
+
"fixed_statement": ("body",),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_BODY_OWNERS = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"method_declaration",
|
|
38
|
+
"constructor_declaration",
|
|
39
|
+
"destructor_declaration",
|
|
40
|
+
"operator_declaration",
|
|
41
|
+
"conversion_operator_declaration",
|
|
42
|
+
"local_function_statement",
|
|
43
|
+
"accessor_declaration",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_MODIFIER_OWNERS = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"class_declaration",
|
|
50
|
+
"struct_declaration",
|
|
51
|
+
"interface_declaration",
|
|
52
|
+
"record_declaration",
|
|
53
|
+
"enum_declaration",
|
|
54
|
+
"delegate_declaration",
|
|
55
|
+
"method_declaration",
|
|
56
|
+
"constructor_declaration",
|
|
57
|
+
"destructor_declaration",
|
|
58
|
+
"operator_declaration",
|
|
59
|
+
"conversion_operator_declaration",
|
|
60
|
+
"property_declaration",
|
|
61
|
+
"indexer_declaration",
|
|
62
|
+
"event_declaration",
|
|
63
|
+
"event_field_declaration",
|
|
64
|
+
"field_declaration",
|
|
65
|
+
"local_function_statement",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VerificationError(Exception):
|
|
71
|
+
"""The formatted text would not mean what the source meant."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def verify(before: SourceFile, after: SourceFile) -> None:
|
|
75
|
+
left = canonical(before.root)
|
|
76
|
+
right = canonical(after.root)
|
|
77
|
+
if left != right:
|
|
78
|
+
raise VerificationError(f"the formatted code differs from the source ({_first_difference(left, right)})")
|
|
79
|
+
if comment_words(before) != comment_words(after):
|
|
80
|
+
missing = comment_words(before) - comment_words(after)
|
|
81
|
+
extra = comment_words(after) - comment_words(before)
|
|
82
|
+
detail = next(iter(missing or extra))
|
|
83
|
+
raise VerificationError(f"the comments changed ({detail!r})")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def comment_words(file: SourceFile) -> Counter[str]:
|
|
87
|
+
words: Counter[str] = Counter()
|
|
88
|
+
for comment in _all_comments(file):
|
|
89
|
+
text = comment.text
|
|
90
|
+
if text.startswith("///") and not text.startswith("////"):
|
|
91
|
+
words["///" + text[3:].strip()] += 1
|
|
92
|
+
elif text.startswith("//"):
|
|
93
|
+
words.update(text[2:].split())
|
|
94
|
+
else:
|
|
95
|
+
lines = [line.strip() for line in text.splitlines()]
|
|
96
|
+
words["/*" + "\n".join(lines)] += 1
|
|
97
|
+
return words
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _all_comments(file: SourceFile) -> list[Comment]:
|
|
101
|
+
comments: list[Comment] = list(file.trailing_comments)
|
|
102
|
+
for token in file.tokens:
|
|
103
|
+
comments.extend(token.leading)
|
|
104
|
+
comments.extend(token.trailing)
|
|
105
|
+
return comments
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def canonical(node: Node) -> Canon:
|
|
109
|
+
if node.text is not None:
|
|
110
|
+
text = node.text.strip() if node.type == "preproc_arg" else node.text
|
|
111
|
+
return (node.type, text)
|
|
112
|
+
if node.type == "parenthesized_expression":
|
|
113
|
+
inner = [c for c in node.children if c.named]
|
|
114
|
+
if len(inner) == 1 and node.children[0].text == "(" and node.children[-1].text == ")":
|
|
115
|
+
return canonical(inner[0])
|
|
116
|
+
children = list(node.children)
|
|
117
|
+
if node.type == "compilation_unit":
|
|
118
|
+
children = _file_scoped(children)
|
|
119
|
+
if node.type in _BODY_OWNERS:
|
|
120
|
+
children = _expression_body(children)
|
|
121
|
+
if node.type in ("property_declaration", "indexer_declaration"):
|
|
122
|
+
children = _property_arrow(children)
|
|
123
|
+
out: list[object] = []
|
|
124
|
+
i = 0
|
|
125
|
+
while i < len(children):
|
|
126
|
+
child = children[i]
|
|
127
|
+
if child.type in ("preproc_region", "preproc_endregion"):
|
|
128
|
+
i += 1
|
|
129
|
+
continue
|
|
130
|
+
if child.type == "attribute_list" or child.type == "global_attribute":
|
|
131
|
+
out.extend(_split_attributes(child))
|
|
132
|
+
elif child.type == "using_directive":
|
|
133
|
+
run = []
|
|
134
|
+
while i < len(children) and children[i].type == "using_directive":
|
|
135
|
+
run.append(canonical(children[i]))
|
|
136
|
+
i += 1
|
|
137
|
+
out.extend(sorted(run, key=repr))
|
|
138
|
+
continue
|
|
139
|
+
elif child.type == "modifier" and node.type in _MODIFIER_OWNERS:
|
|
140
|
+
run = []
|
|
141
|
+
while i < len(children) and children[i].type == "modifier":
|
|
142
|
+
run.append(canonical(children[i]))
|
|
143
|
+
i += 1
|
|
144
|
+
out.extend(sorted(run, key=repr))
|
|
145
|
+
continue
|
|
146
|
+
elif (
|
|
147
|
+
child.text == ","
|
|
148
|
+
and i == len(children) - 2
|
|
149
|
+
and children[-1].text in ("}", "]")
|
|
150
|
+
and node.type != "array_rank_specifier"
|
|
151
|
+
):
|
|
152
|
+
pass
|
|
153
|
+
elif node.type in _EMBEDDED and is_embedded(node, child) and _needs_braces(node, child):
|
|
154
|
+
out.append(("block", (("{", "{"), canonical(child), ("}", "}"))))
|
|
155
|
+
else:
|
|
156
|
+
out.append(canonical(child))
|
|
157
|
+
i += 1
|
|
158
|
+
return (node.type, tuple(out))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_embedded(parent: Node, child: Node) -> bool:
|
|
162
|
+
"""Whether ``child`` is a statement embedded in ``parent``, such as an if's body."""
|
|
163
|
+
fields = _EMBEDDED.get(parent.type)
|
|
164
|
+
if fields is None:
|
|
165
|
+
return False
|
|
166
|
+
if child.field in fields:
|
|
167
|
+
return True
|
|
168
|
+
# lock and fixed statements keep their body as an unnamed last child.
|
|
169
|
+
return (
|
|
170
|
+
parent.type in ("lock_statement", "fixed_statement")
|
|
171
|
+
and child is parent.children[-1]
|
|
172
|
+
and child.named
|
|
173
|
+
and child.type.endswith(("statement", "block"))
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _needs_braces(parent: Node, child: Node) -> bool:
|
|
178
|
+
if child.type == "block":
|
|
179
|
+
return False
|
|
180
|
+
if parent.type == "if_statement" and child.field == "alternative" and child.type == "if_statement":
|
|
181
|
+
return False
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _split_attributes(node: Node) -> list[Canon]:
|
|
186
|
+
"""``[t: A, B]`` as ``[t: A]`` ``[t: B]``."""
|
|
187
|
+
target: list[Canon] = []
|
|
188
|
+
attributes: list[Node] = []
|
|
189
|
+
for child in node.children:
|
|
190
|
+
if child.type == "attribute":
|
|
191
|
+
attributes.append(child)
|
|
192
|
+
elif child.type == "attribute_target_specifier":
|
|
193
|
+
target.append(canonical(child))
|
|
194
|
+
elif child.text not in ("[", "]", ","):
|
|
195
|
+
target.append(canonical(child))
|
|
196
|
+
if not attributes:
|
|
197
|
+
return [canonical(node)]
|
|
198
|
+
return [("attribute_list", (("[", "["), *target, canonical(a), ("]", "]"))) for a in attributes]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _expression_body(children: list[Node]) -> list[Node]:
|
|
202
|
+
"""A block body of one return, expression or throw statement, in arrow form."""
|
|
203
|
+
for index, child in enumerate(children):
|
|
204
|
+
if child.field != "body" or child.type != "block":
|
|
205
|
+
continue
|
|
206
|
+
statements = child.children[1:-1]
|
|
207
|
+
if len(statements) != 1:
|
|
208
|
+
return children
|
|
209
|
+
statement = statements[0]
|
|
210
|
+
expression = _arrow_expression(statement)
|
|
211
|
+
if expression is None:
|
|
212
|
+
return children
|
|
213
|
+
arrow = Node(type="arrow_expression_clause", field="body")
|
|
214
|
+
arrow.children = [Node(type="=>", named=False, text="=>"), expression]
|
|
215
|
+
semicolon = Node(type=";", named=False, text=";")
|
|
216
|
+
rest = [c for c in children[index + 1 :] if c.text != ";"]
|
|
217
|
+
return [*children[:index], arrow, semicolon, *rest]
|
|
218
|
+
return children
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _arrow_expression(statement: Node) -> Node | None:
|
|
222
|
+
kids = statement.children
|
|
223
|
+
if statement.type == "expression_statement" and len(kids) == 2 and kids[1].text == ";":
|
|
224
|
+
return kids[0]
|
|
225
|
+
# return x; and return this;, not return ref x;
|
|
226
|
+
if statement.type == "return_statement" and len(kids) == 3 and kids[2].text == ";" and kids[1].text != "ref":
|
|
227
|
+
return kids[1]
|
|
228
|
+
if statement.type == "throw_statement" and len(kids) == 3 and kids[2].text == ";":
|
|
229
|
+
thrown = Node(type="throw_expression")
|
|
230
|
+
thrown.children = [kids[0], kids[1]]
|
|
231
|
+
return thrown
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _property_arrow(children: list[Node]) -> list[Node]:
|
|
236
|
+
"""A property with only a plain getter, in arrow form."""
|
|
237
|
+
for index, child in enumerate(children):
|
|
238
|
+
if child.type != "accessor_list":
|
|
239
|
+
continue
|
|
240
|
+
accessors = [c for c in child.children if c.type == "accessor_declaration"]
|
|
241
|
+
others = [c for c in child.children if c.type != "accessor_declaration" and c.text not in ("{", "}")]
|
|
242
|
+
if len(accessors) != 1 or others or index != len(children) - 1:
|
|
243
|
+
return children
|
|
244
|
+
accessor = accessors[0]
|
|
245
|
+
if [c.text for c in accessor.children if c.type != "block" and c.type != "arrow_expression_clause"] not in (
|
|
246
|
+
["get"],
|
|
247
|
+
["get", ";"],
|
|
248
|
+
):
|
|
249
|
+
return children
|
|
250
|
+
body = _expression_body(accessor.children)
|
|
251
|
+
arrow = next((c for c in body if c.type == "arrow_expression_clause"), None)
|
|
252
|
+
if arrow is None:
|
|
253
|
+
return children
|
|
254
|
+
semicolon = Node(type=";", named=False, text=";")
|
|
255
|
+
return [*children[:index], arrow, semicolon]
|
|
256
|
+
return children
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _file_scoped(children: list[Node]) -> list[Node]:
|
|
260
|
+
"""The one block namespace of a file, as a file-scoped namespace."""
|
|
261
|
+
namespaces = [c for c in children if c.type == "namespace_declaration"]
|
|
262
|
+
if len(namespaces) != 1 or any(c.type == "file_scoped_namespace_declaration" for c in children):
|
|
263
|
+
return children
|
|
264
|
+
namespace = namespaces[0]
|
|
265
|
+
index = children.index(namespace)
|
|
266
|
+
allowed = ("using_directive", "extern_alias_directive", "global_attribute")
|
|
267
|
+
if any(c.type not in allowed and not c.type.startswith("preproc") for c in children[:index]):
|
|
268
|
+
return children
|
|
269
|
+
if any(not c.type.startswith("preproc") for c in children[index + 1 :]):
|
|
270
|
+
return children
|
|
271
|
+
body = namespace.child("body")
|
|
272
|
+
if body is None or body.type != "declaration_list" or len(body.children) < 2:
|
|
273
|
+
return children
|
|
274
|
+
members = body.children[1:-1]
|
|
275
|
+
if any(_contains_namespace(m) for m in members):
|
|
276
|
+
return children
|
|
277
|
+
head = [c for c in namespace.children if c is not body]
|
|
278
|
+
if any(c.text == ";" for c in head):
|
|
279
|
+
return children
|
|
280
|
+
scoped = Node(type="file_scoped_namespace_declaration")
|
|
281
|
+
scoped.children = [*head, Node(type=";", named=False, text=";")]
|
|
282
|
+
return [*children[:index], scoped, *members, *children[index + 1 :]]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _contains_namespace(node: Node) -> bool:
|
|
286
|
+
stack = [node]
|
|
287
|
+
while stack:
|
|
288
|
+
current = stack.pop()
|
|
289
|
+
if current.type in ("namespace_declaration", "file_scoped_namespace_declaration"):
|
|
290
|
+
return True
|
|
291
|
+
stack.extend(current.children)
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _first_difference(left: object, right: object, path: Sequence[str] = ()) -> str:
|
|
296
|
+
if isinstance(left, tuple) and isinstance(right, tuple):
|
|
297
|
+
if len(left) == 2 and len(right) == 2 and isinstance(left[1], str) and isinstance(right[1], str):
|
|
298
|
+
if left != right:
|
|
299
|
+
return f"{'/'.join(path)}: {left[1]!r} became {right[1]!r}"
|
|
300
|
+
return ""
|
|
301
|
+
if len(left) == 2 and len(right) == 2 and left[0] == right[0]:
|
|
302
|
+
a, b = left[1], right[1]
|
|
303
|
+
if isinstance(a, tuple) and isinstance(b, tuple):
|
|
304
|
+
for x, y in zip(a, b, strict=False):
|
|
305
|
+
found = _first_difference(x, y, (*path, str(left[0])))
|
|
306
|
+
if found:
|
|
307
|
+
return found
|
|
308
|
+
if len(a) != len(b):
|
|
309
|
+
return f"{'/'.join((*path, str(left[0])))}: {len(a)} children became {len(b)}"
|
|
310
|
+
return ""
|
|
311
|
+
return f"{'/'.join(path)}: {left[0]} became {right[0]}"
|
|
312
|
+
return "/".join(path)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prettycsharp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An opinionated formatter for C#, in the spirit of Prettier
|
|
5
|
+
Project-URL: Homepage, https://github.com/WilliamSmithEdward/prettyCSharp
|
|
6
|
+
Project-URL: Repository, https://github.com/WilliamSmithEdward/prettyCSharp
|
|
7
|
+
Project-URL: Issues, https://github.com/WilliamSmithEdward/prettyCSharp/issues
|
|
8
|
+
Author: William Smith Edward
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: c#,code-style,csharp,dotnet,formatter,prettier,tree-sitter
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
27
|
+
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
28
|
+
Requires-Dist: tree-sitter-c-sharp>=0.23.1
|
|
29
|
+
Requires-Dist: tree-sitter>=0.25
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy==2.4.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff==0.17.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# prettycsharp
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/prettycsharp/)
|
|
39
|
+
[](https://pypi.org/project/prettycsharp/)
|
|
40
|
+
[](https://pepy.tech/projects/prettycsharp)
|
|
41
|
+
[](https://github.com/WilliamSmithEdward/prettyCSharp/actions/workflows/ci.yml)
|
|
42
|
+
[](https://github.com/WilliamSmithEdward/prettyCSharp/actions/workflows/security.yml)
|
|
43
|
+
[](https://github.com/WilliamSmithEdward/prettyCSharp/actions/workflows/malware-scan.yml)
|
|
44
|
+
[](https://scorecard.dev/viewer/?uri=github.com/WilliamSmithEdward/prettyCSharp)
|
|
45
|
+
[](https://github.com/WilliamSmithEdward/prettyCSharp/blob/main/LICENSE)
|
|
46
|
+
|
|
47
|
+
An opinionated formatter for C#, written in Python, in the spirit of Prettier.
|
|
48
|
+
It parses each file with [tree-sitter](https://tree-sitter.github.io/) and
|
|
49
|
+
reprints the whole file in one consistent layout: braces, indentation, line
|
|
50
|
+
breaks, blank lines, alignment and a few safe rewrites such as file-scoped
|
|
51
|
+
namespaces, sorted usings and expression-bodied members. It needs no .NET
|
|
52
|
+
SDK.
|
|
53
|
+
|
|
54
|
+
Before:
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
using System;
|
|
58
|
+
using Microsoft.Extensions.Logging;
|
|
59
|
+
namespace Shop.Orders {
|
|
60
|
+
public class OrderService {
|
|
61
|
+
static private readonly int MaxRetries=3;
|
|
62
|
+
public decimal Total(Order order) { return order.Lines.Where(l => l.IsTaxable).Sum(l => l.Amount); }
|
|
63
|
+
public void Ship(Order order) {
|
|
64
|
+
if (order == null) return;
|
|
65
|
+
if (order.IsPaid) { Send(order); } else { Hold(order); }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After:
|
|
72
|
+
|
|
73
|
+
```csharp
|
|
74
|
+
using Microsoft.Extensions.Logging;
|
|
75
|
+
using System;
|
|
76
|
+
|
|
77
|
+
namespace Shop.Orders;
|
|
78
|
+
|
|
79
|
+
public class OrderService
|
|
80
|
+
{
|
|
81
|
+
private static readonly int MaxRetries = 3;
|
|
82
|
+
|
|
83
|
+
public decimal Total(Order order) =>
|
|
84
|
+
order.Lines
|
|
85
|
+
.Where(l => l.IsTaxable)
|
|
86
|
+
.Sum(l => l.Amount);
|
|
87
|
+
|
|
88
|
+
public void Ship(Order order)
|
|
89
|
+
{
|
|
90
|
+
if (order == null) return;
|
|
91
|
+
|
|
92
|
+
if (order.IsPaid)
|
|
93
|
+
{
|
|
94
|
+
Send(order);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
else
|
|
98
|
+
{
|
|
99
|
+
Hold(order);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
Python 3.10 or later.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install prettycsharp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Use
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
prettycsharp format src/
|
|
117
|
+
```
|
|
118
|
+
```bash
|
|
119
|
+
prettycsharp check src/
|
|
120
|
+
```
|
|
121
|
+
```bash
|
|
122
|
+
prettycsharp diff Order.cs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`format` rewrites files in place, `check` exits 1 when any file would
|
|
126
|
+
change, and `diff` prints a unified diff. `--write`, `--check` and `--diff`
|
|
127
|
+
are aliases. Given one file and no command, the formatted file goes to
|
|
128
|
+
stdout; `-` formats stdin to stdout, with `--stdin-filepath` naming the
|
|
129
|
+
path whose settings apply.
|
|
130
|
+
|
|
131
|
+
A target can be a `.cs` file, a folder, a `.csproj` (the files it
|
|
132
|
+
compiles, after its `Compile Include` and `Remove` items) or a `.sln` or
|
|
133
|
+
`.slnx` (every C# project in it). Folders are searched for `.cs` files.
|
|
134
|
+
|
|
135
|
+
| Option | Meaning |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `-o KEY=VALUE`, `--option` | Set one setting, over every settings file. Repeatable. |
|
|
138
|
+
| `--config FILE` | Use this settings file instead of searching for one. |
|
|
139
|
+
| `--no-ignore` | Do not skip what `.gitignore` and `.prettycsharpignore` list. |
|
|
140
|
+
| `--stdin-filepath PATH` | The path stdin stands for. |
|
|
141
|
+
|
|
142
|
+
Exit status: 0 when nothing needs to change, 1 when `check` or `diff` found
|
|
143
|
+
a file that would change, 2 when a file could not be read, parsed or
|
|
144
|
+
formatted, or a setting is wrong. A file that does not parse is reported
|
|
145
|
+
and left untouched.
|
|
146
|
+
|
|
147
|
+
From Python:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_source
|
|
151
|
+
|
|
152
|
+
formatted = format_source(source)
|
|
153
|
+
narrow = format_source(source, Settings().with_values({"max_line_length": 100}))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Settings
|
|
157
|
+
|
|
158
|
+
Settings come from, highest precedence first:
|
|
159
|
+
|
|
160
|
+
1. `-o` options on the command line;
|
|
161
|
+
2. the nearest `.prettycsharp.toml`, or `pyproject.toml` with a
|
|
162
|
+
`[tool.prettycsharp]` table, found by walking up from each file;
|
|
163
|
+
3. the standard keys of `.editorconfig` (`indent_style`, `indent_size`,
|
|
164
|
+
`max_line_length`, `end_of_line`, `charset`, `insert_final_newline`,
|
|
165
|
+
and the Roslyn keys for braces, `else`/`catch`/`finally` placement, using
|
|
166
|
+
order, namespace style, braces and modifier order);
|
|
167
|
+
4. the defaults.
|
|
168
|
+
|
|
169
|
+
```toml
|
|
170
|
+
# .prettycsharp.toml
|
|
171
|
+
max_line_length = 100
|
|
172
|
+
brace_style = "kr"
|
|
173
|
+
continuation_keywords = "same_line"
|
|
174
|
+
align_assignments = false
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Every setting, its default, its alternatives and an example of each layout
|
|
178
|
+
are in
|
|
179
|
+
[DESIGN.md](https://github.com/WilliamSmithEdward/prettyCSharp/blob/main/DESIGN.md).
|
|
180
|
+
|
|
181
|
+
## Skipping files and code
|
|
182
|
+
|
|
183
|
+
`bin/`, `obj/`, hidden folders and generated files (`*.g.cs`,
|
|
184
|
+
`*.Designer.cs`, and files marked `<auto-generated>`) are always skipped.
|
|
185
|
+
So is whatever `.gitignore` lists inside a git repository, and whatever a
|
|
186
|
+
`.prettycsharpignore` file lists, in gitignore syntax.
|
|
187
|
+
|
|
188
|
+
Inside a file, `// prettycsharp-ignore` leaves the next statement or member
|
|
189
|
+
as written, and `// prettycsharp-ignore-start` ... `// prettycsharp-ignore-end`
|
|
190
|
+
leaves everything between them as written.
|
|
191
|
+
|
|
192
|
+
## How it stays safe
|
|
193
|
+
|
|
194
|
+
Every output is parsed again and compared with the input as syntax trees,
|
|
195
|
+
with only the changes the settings allow (parentheses, modifier and using
|
|
196
|
+
order, attribute lists, trailing commas, expression bodies, namespace style,
|
|
197
|
+
braces and regions) taken out of the comparison. The comments must hold the
|
|
198
|
+
same words. If anything else differs, the file is not changed and the
|
|
199
|
+
command reports the difference. String literals, including verbatim, raw and
|
|
200
|
+
interpolated strings, are printed exactly as written.
|
|
201
|
+
|
|
202
|
+
## Limits
|
|
203
|
+
|
|
204
|
+
prettycsharp reads C# through tree-sitter's C# grammar, which parses every
|
|
205
|
+
`#if` branch as code. A declaration or statement split between `#if` and
|
|
206
|
+
`#else` therefore does not parse, and neither does syntax newer than the
|
|
207
|
+
grammar (such as C# 14's null-conditional assignment). Such files are
|
|
208
|
+
reported with their line and column and left untouched.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
[MIT](https://github.com/WilliamSmithEdward/prettyCSharp/blob/main/LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
prettycsharp/__init__.py,sha256=2YPCftqUxMqYgSCIdxolZEgiJUTzze9bg7Lfukgv1vY,378
|
|
2
|
+
prettycsharp/__main__.py,sha256=YUL16nU2FBOx2nJWBvuMaqSUo73TWH4tEyat839_TIw,64
|
|
3
|
+
prettycsharp/cli.py,sha256=yEsWvztAd9ulnnkC5MNU5vHTVBq-D3zrPnV3M_qtPhw,9949
|
|
4
|
+
prettycsharp/config.py,sha256=uxyv-fESIrJewN9203MHCdx9NVeUNKT7BucXjoVQt_s,9221
|
|
5
|
+
prettycsharp/doc.py,sha256=3_b-qNVEbf_PodyBp59lMfCIz-8jSAu24OdfF6IafHM,20097
|
|
6
|
+
prettycsharp/formatter.py,sha256=hjTFhtvd1rQlVpYLRBmcM9hsmNqjihPD4tPpv2ed2OQ,3808
|
|
7
|
+
prettycsharp/ignore.py,sha256=gVltaT7c6IbA18AHW5w1EH6LvmG9vPfSVfze0wtpyPg,6205
|
|
8
|
+
prettycsharp/parsing.py,sha256=jBKIM4c8quIB0WAER1i0GgTkD7S-zcgJuBlGadfajtI,1809
|
|
9
|
+
prettycsharp/project.py,sha256=mzps6K-_vHBfWjBiltZiIXSwbmTKu0HHMOM0XKfKlnE,7178
|
|
10
|
+
prettycsharp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
prettycsharp/rewrites.py,sha256=lrIJFmP3QkmmQLEBZcenljttrBZzgp8NJf50a1Ta7pw,27295
|
|
12
|
+
prettycsharp/settings.py,sha256=4Zo_dxUyZHFA0VqZQPmtFV88Y_1_zUwjbX6s7jEBViw,7037
|
|
13
|
+
prettycsharp/syntax.py,sha256=jzxS7JeYhrn4vNbO7UetDIm42ur3X3I5ESsnaDqd03A,12378
|
|
14
|
+
prettycsharp/verify.py,sha256=usnNWXu68bu3Wz-dafg7pA1VbNMj7rTlMwYbPI9oEgw,12210
|
|
15
|
+
prettycsharp/printer/__init__.py,sha256=Uz_MD6_IaW9GWuZXSQIX2pW3UDdwrig3e1d4ZMfAsKE,994
|
|
16
|
+
prettycsharp/printer/core.py,sha256=ugiGlig7u064cwUiUy5x_mHBKUgDeF8cBzSnaX-sEcU,20295
|
|
17
|
+
prettycsharp/printer/declarations.py,sha256=M3jsLOpY-GOPvPKynETb4Yt8ASla-sF16s8ykmcm_2s,26246
|
|
18
|
+
prettycsharp/printer/expressions.py,sha256=jOOuo9kp62fEzqexv5LwqzQHvvgsqh1_dCjJlnbWFdc,45581
|
|
19
|
+
prettycsharp/printer/statements.py,sha256=x7PhyAMfrnMQVDMhB-HM68tonq5EsceSvVguMznIQ3Y,26618
|
|
20
|
+
prettycsharp-1.0.0.dist-info/METADATA,sha256=wHaFQWHgVZbOtWkvtT8yg_Mr1RTpmgFbHupnY56nUwA,7922
|
|
21
|
+
prettycsharp-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
22
|
+
prettycsharp-1.0.0.dist-info/entry_points.txt,sha256=LezlCMNm4uvzhnLmoneMBwVwBbA7ye1HJSPlO8JI3Uc,55
|
|
23
|
+
prettycsharp-1.0.0.dist-info/licenses/LICENSE,sha256=iwoVDiLOSUadguAENb1gGfsHgtnNubULiepCUeG7RXw,1077
|
|
24
|
+
prettycsharp-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Smith Edward
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|