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/doc.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""The layout document model and the printer that fits it to a line width.
|
|
2
|
+
|
|
3
|
+
A document describes text with optional line breaks, in the style of
|
|
4
|
+
Wadler's "prettier printer" as Prettier implements it. A ``Group`` prints on
|
|
5
|
+
one line when it fits and breaks every ``Line`` directly inside it when it
|
|
6
|
+
does not. ``HARDLINE`` always breaks and forces every enclosing group to
|
|
7
|
+
break. The printer is iterative, so deeply nested documents do not exhaust
|
|
8
|
+
the Python stack.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
# A document is a str, a list of documents, or one of the node classes below.
|
|
17
|
+
Doc = object
|
|
18
|
+
|
|
19
|
+
_MODE_BREAK = 0
|
|
20
|
+
_MODE_FLAT = 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(eq=False, slots=True)
|
|
24
|
+
class Group:
|
|
25
|
+
contents: Doc
|
|
26
|
+
should_break: bool = False
|
|
27
|
+
id: object | None = None
|
|
28
|
+
# A conditional group: the printer uses the first state that fits, and
|
|
29
|
+
# the last one when none does.
|
|
30
|
+
expanded_states: Sequence[Doc] | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(eq=False, slots=True)
|
|
34
|
+
class Indent:
|
|
35
|
+
contents: Doc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(eq=False, slots=True)
|
|
39
|
+
class Align:
|
|
40
|
+
"""Indent the contents by ``spaces`` extra columns."""
|
|
41
|
+
|
|
42
|
+
spaces: int
|
|
43
|
+
contents: Doc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(eq=False, slots=True)
|
|
47
|
+
class Line:
|
|
48
|
+
hard: bool = False
|
|
49
|
+
soft: bool = False
|
|
50
|
+
literal: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(eq=False, slots=True)
|
|
54
|
+
class IfBreak:
|
|
55
|
+
break_contents: Doc
|
|
56
|
+
flat_contents: Doc = ""
|
|
57
|
+
group_id: object | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(eq=False, slots=True)
|
|
61
|
+
class IndentIfBreak:
|
|
62
|
+
contents: Doc
|
|
63
|
+
group_id: object
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(eq=False, slots=True)
|
|
67
|
+
class LineSuffix:
|
|
68
|
+
"""Text deferred to the end of the line, such as a trailing comment."""
|
|
69
|
+
|
|
70
|
+
contents: Doc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(eq=False, slots=True)
|
|
74
|
+
class BreakParent:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(eq=False, slots=True)
|
|
79
|
+
class EnsureLine:
|
|
80
|
+
"""A line break, unless nothing but indentation is on the line yet.
|
|
81
|
+
|
|
82
|
+
Comments that stood on lines of their own come after one, so they never
|
|
83
|
+
end up behind code, where the next formatting would take them for
|
|
84
|
+
trailing comments.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(eq=False, slots=True)
|
|
89
|
+
class Fill:
|
|
90
|
+
"""Content, separator, content, ...: separators break only where needed."""
|
|
91
|
+
|
|
92
|
+
parts: list[Doc] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(eq=False, slots=True)
|
|
96
|
+
class Wrapped:
|
|
97
|
+
"""Words that wrap to the line width at print time, each line prefixed.
|
|
98
|
+
|
|
99
|
+
Used for comment paragraphs: ``prefix`` is ``"// "`` and the words are
|
|
100
|
+
laid out greedily, so the result depends on the indentation the printer
|
|
101
|
+
reaches, which is known only while printing.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
prefix: str
|
|
105
|
+
words: list[str]
|
|
106
|
+
# The lines as written. When given, they are printed unchanged unless
|
|
107
|
+
# one of them would pass the line width; only then are the words wrapped.
|
|
108
|
+
lines: list[str] | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(eq=False, slots=True)
|
|
112
|
+
class TrimIndent:
|
|
113
|
+
"""Remove the indentation printed since the last line break: go to column 0."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
LINE = Line()
|
|
117
|
+
SOFTLINE = Line(soft=True)
|
|
118
|
+
ENSURE_LINE = EnsureLine()
|
|
119
|
+
TRIM_INDENT = TrimIndent()
|
|
120
|
+
_HARD = Line(hard=True)
|
|
121
|
+
_BREAK_PARENT = BreakParent()
|
|
122
|
+
HARDLINE: Doc = [_HARD, _BREAK_PARENT]
|
|
123
|
+
LITERALLINE: Doc = [Line(hard=True, literal=True), _BREAK_PARENT]
|
|
124
|
+
BREAK_PARENT = _BREAK_PARENT
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def group(contents: Doc, should_break: bool = False, id: object | None = None) -> Group:
|
|
128
|
+
return Group(contents, should_break, id)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def conditional_group(states: Sequence[Doc]) -> Group:
|
|
132
|
+
return Group(states[0], False, None, list(states))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def indent(contents: Doc) -> Indent:
|
|
136
|
+
return Indent(contents)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def join(separator: Doc, docs: Sequence[Doc]) -> list[Doc]:
|
|
140
|
+
out: list[Doc] = []
|
|
141
|
+
for i, doc in enumerate(docs):
|
|
142
|
+
if i:
|
|
143
|
+
out.append(separator)
|
|
144
|
+
out.append(doc)
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def propagate_breaks(doc: Doc) -> None:
|
|
149
|
+
"""Mark every group that contains a hard break as broken.
|
|
150
|
+
|
|
151
|
+
A conditional group stops the propagation, as in Prettier: its states
|
|
152
|
+
decide for themselves how much of them may break.
|
|
153
|
+
"""
|
|
154
|
+
# Each stack entry is (doc, exiting). Groups are tracked in group_stack.
|
|
155
|
+
group_stack: list[Group] = []
|
|
156
|
+
stack: list[tuple[Doc, bool]] = [(doc, False)]
|
|
157
|
+
seen: set[int] = set()
|
|
158
|
+
while stack:
|
|
159
|
+
node, exiting = stack.pop()
|
|
160
|
+
if exiting:
|
|
161
|
+
assert isinstance(node, Group)
|
|
162
|
+
group_stack.pop()
|
|
163
|
+
if node.should_break and group_stack:
|
|
164
|
+
_break_parent(group_stack)
|
|
165
|
+
continue
|
|
166
|
+
if isinstance(node, str):
|
|
167
|
+
continue
|
|
168
|
+
if isinstance(node, list):
|
|
169
|
+
for child in reversed(node):
|
|
170
|
+
stack.append((child, False))
|
|
171
|
+
elif isinstance(node, BreakParent):
|
|
172
|
+
if group_stack:
|
|
173
|
+
_break_parent(group_stack)
|
|
174
|
+
elif isinstance(node, Group):
|
|
175
|
+
if id(node) in seen:
|
|
176
|
+
# A group shared between conditional states: its own break
|
|
177
|
+
# flag is already settled; still tell the parent.
|
|
178
|
+
if node.should_break and group_stack:
|
|
179
|
+
_break_parent(group_stack)
|
|
180
|
+
continue
|
|
181
|
+
seen.add(id(node))
|
|
182
|
+
group_stack.append(node)
|
|
183
|
+
stack.append((node, True))
|
|
184
|
+
if node.expanded_states is not None:
|
|
185
|
+
for state in reversed(node.expanded_states):
|
|
186
|
+
stack.append((state, False))
|
|
187
|
+
else:
|
|
188
|
+
stack.append((node.contents, False))
|
|
189
|
+
elif isinstance(node, (Indent, Align, LineSuffix, IndentIfBreak)):
|
|
190
|
+
stack.append((node.contents, False))
|
|
191
|
+
elif isinstance(node, IfBreak):
|
|
192
|
+
stack.append((node.flat_contents, False))
|
|
193
|
+
stack.append((node.break_contents, False))
|
|
194
|
+
elif isinstance(node, Fill):
|
|
195
|
+
for child in reversed(node.parts):
|
|
196
|
+
stack.append((child, False))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _break_parent(group_stack: list[Group]) -> None:
|
|
200
|
+
parent = group_stack[-1]
|
|
201
|
+
if parent.expanded_states is None and not parent.should_break:
|
|
202
|
+
parent.should_break = True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(slots=True)
|
|
206
|
+
class _Indentation:
|
|
207
|
+
text: str
|
|
208
|
+
width: int
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Printer:
|
|
212
|
+
def __init__(self, width: int, indent_unit: str, indent_width: int, eol: str) -> None:
|
|
213
|
+
self.width = width
|
|
214
|
+
self.indent_unit = indent_unit
|
|
215
|
+
self.indent_width = indent_width
|
|
216
|
+
self.eol = eol
|
|
217
|
+
self._indent_cache: dict[tuple[int, int], _Indentation] = {}
|
|
218
|
+
|
|
219
|
+
def _indentation(self, base: _Indentation, *, levels: int = 0, spaces: int = 0) -> _Indentation:
|
|
220
|
+
return _Indentation(
|
|
221
|
+
base.text + self.indent_unit * levels + " " * spaces,
|
|
222
|
+
base.width + self.indent_width * levels + spaces,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def print(self, doc: Doc) -> str:
|
|
226
|
+
propagate_breaks(doc)
|
|
227
|
+
root = _Indentation("", 0)
|
|
228
|
+
out: list[str] = []
|
|
229
|
+
pos = 0
|
|
230
|
+
line_suffix: list[tuple[_Indentation, int, Doc]] = []
|
|
231
|
+
group_modes: dict[object, int] = {}
|
|
232
|
+
cmds: list[tuple[_Indentation, int, Doc]] = [(root, _MODE_BREAK, doc)]
|
|
233
|
+
# Whether anything but whitespace is on the current line yet.
|
|
234
|
+
content = False
|
|
235
|
+
|
|
236
|
+
while cmds:
|
|
237
|
+
ind, mode, node = cmds.pop()
|
|
238
|
+
if isinstance(node, str):
|
|
239
|
+
if line_suffix and node.strip():
|
|
240
|
+
# Code after an end-of-line comment goes on the next line,
|
|
241
|
+
# so the comment stays behind the code it followed.
|
|
242
|
+
cmds.append((ind, mode, node))
|
|
243
|
+
cmds.append((ind, mode, _HARD))
|
|
244
|
+
continue
|
|
245
|
+
out.append(node)
|
|
246
|
+
newline = node.rfind("\n")
|
|
247
|
+
if newline >= 0:
|
|
248
|
+
pos = len(node) - newline - 1
|
|
249
|
+
content = bool(node[newline + 1 :].strip())
|
|
250
|
+
else:
|
|
251
|
+
pos += len(node)
|
|
252
|
+
content = content or bool(node.strip())
|
|
253
|
+
elif isinstance(node, TrimIndent):
|
|
254
|
+
if not content:
|
|
255
|
+
_trim(out)
|
|
256
|
+
pos = 0
|
|
257
|
+
elif isinstance(node, EnsureLine):
|
|
258
|
+
if content:
|
|
259
|
+
cmds.append((ind, mode, _HARD))
|
|
260
|
+
elif line_suffix:
|
|
261
|
+
# End-of-line comments with no code left on their line:
|
|
262
|
+
# each takes a line of its own, at this indentation.
|
|
263
|
+
_trim(out)
|
|
264
|
+
if out and out[-1].endswith(("\n", "\r")):
|
|
265
|
+
out.append(ind.text)
|
|
266
|
+
pos = ind.width
|
|
267
|
+
for _, suffix_mode, suffix in reversed(line_suffix):
|
|
268
|
+
cmds.append((ind, suffix_mode, _HARD))
|
|
269
|
+
cmds.append((ind, suffix_mode, _without_leading_space(suffix)))
|
|
270
|
+
line_suffix = []
|
|
271
|
+
elif out:
|
|
272
|
+
# Only spaces since the line break: start at this indentation.
|
|
273
|
+
_trim(out)
|
|
274
|
+
if out and out[-1].endswith(("\n", "\r")):
|
|
275
|
+
out.append(ind.text)
|
|
276
|
+
pos = ind.width
|
|
277
|
+
elif isinstance(node, list):
|
|
278
|
+
for child in reversed(node):
|
|
279
|
+
cmds.append((ind, mode, child))
|
|
280
|
+
elif isinstance(node, Indent):
|
|
281
|
+
cmds.append((self._indentation(ind, levels=1), mode, node.contents))
|
|
282
|
+
elif isinstance(node, Align):
|
|
283
|
+
# A negative width aligns to the column the printer is at.
|
|
284
|
+
spaces = node.spaces if node.spaces >= 0 else max(pos - ind.width, 0)
|
|
285
|
+
cmds.append((self._indentation(ind, spaces=spaces), mode, node.contents))
|
|
286
|
+
elif isinstance(node, Group):
|
|
287
|
+
if mode == _MODE_FLAT and not node.should_break:
|
|
288
|
+
cmds.append((ind, _MODE_FLAT, node.contents))
|
|
289
|
+
if node.id is not None:
|
|
290
|
+
group_modes[node.id] = _MODE_FLAT
|
|
291
|
+
continue
|
|
292
|
+
flat = (ind, _MODE_FLAT, node.contents)
|
|
293
|
+
rem = self.width - pos
|
|
294
|
+
if not node.should_break and self._fits(flat, cmds, rem, group_modes):
|
|
295
|
+
cmds.append(flat)
|
|
296
|
+
chosen = _MODE_FLAT
|
|
297
|
+
elif node.expanded_states is not None:
|
|
298
|
+
most_expanded = node.expanded_states[-1]
|
|
299
|
+
if node.should_break:
|
|
300
|
+
cmds.append((ind, _MODE_BREAK, most_expanded))
|
|
301
|
+
chosen = _MODE_BREAK
|
|
302
|
+
else:
|
|
303
|
+
chosen = _MODE_BREAK
|
|
304
|
+
for state in node.expanded_states[1:]:
|
|
305
|
+
attempt = (ind, _MODE_FLAT, state)
|
|
306
|
+
if self._fits(attempt, cmds, rem, group_modes):
|
|
307
|
+
cmds.append(attempt)
|
|
308
|
+
chosen = _MODE_FLAT
|
|
309
|
+
break
|
|
310
|
+
else:
|
|
311
|
+
cmds.append((ind, _MODE_BREAK, most_expanded))
|
|
312
|
+
else:
|
|
313
|
+
cmds.append((ind, _MODE_BREAK, node.contents))
|
|
314
|
+
chosen = _MODE_BREAK
|
|
315
|
+
if node.id is not None:
|
|
316
|
+
group_modes[node.id] = chosen
|
|
317
|
+
elif isinstance(node, Fill):
|
|
318
|
+
self._print_fill(ind, mode, node, cmds, pos, line_suffix, group_modes)
|
|
319
|
+
elif isinstance(node, IfBreak):
|
|
320
|
+
group_mode = group_modes.get(node.group_id, _MODE_FLAT) if node.group_id is not None else mode
|
|
321
|
+
contents = node.break_contents if group_mode == _MODE_BREAK else node.flat_contents
|
|
322
|
+
cmds.append((ind, mode, contents))
|
|
323
|
+
elif isinstance(node, IndentIfBreak):
|
|
324
|
+
group_mode = group_modes.get(node.group_id, _MODE_FLAT)
|
|
325
|
+
if group_mode == _MODE_BREAK:
|
|
326
|
+
cmds.append((self._indentation(ind, levels=1), mode, node.contents))
|
|
327
|
+
else:
|
|
328
|
+
cmds.append((ind, mode, node.contents))
|
|
329
|
+
elif isinstance(node, LineSuffix):
|
|
330
|
+
if line_suffix:
|
|
331
|
+
# Two end-of-line comments cannot share a line: the first
|
|
332
|
+
# would swallow the second. Break the line between them.
|
|
333
|
+
cmds.append((ind, mode, node))
|
|
334
|
+
cmds.append((ind, mode, _HARD))
|
|
335
|
+
continue
|
|
336
|
+
line_suffix.append((ind, mode, node.contents))
|
|
337
|
+
elif isinstance(node, BreakParent):
|
|
338
|
+
pass
|
|
339
|
+
elif isinstance(node, Wrapped):
|
|
340
|
+
pos = self._print_wrapped(ind, node, out, pos)
|
|
341
|
+
content = True
|
|
342
|
+
elif isinstance(node, Line):
|
|
343
|
+
if mode == _MODE_FLAT and not node.hard:
|
|
344
|
+
if not node.soft:
|
|
345
|
+
out.append(" ")
|
|
346
|
+
pos += 1
|
|
347
|
+
continue
|
|
348
|
+
if line_suffix:
|
|
349
|
+
cmds.append((ind, mode, node))
|
|
350
|
+
for entry in reversed(line_suffix):
|
|
351
|
+
cmds.append(entry)
|
|
352
|
+
line_suffix = []
|
|
353
|
+
# The comment brings its own space.
|
|
354
|
+
_trim(out)
|
|
355
|
+
continue
|
|
356
|
+
_trim(out)
|
|
357
|
+
content = False
|
|
358
|
+
if node.literal:
|
|
359
|
+
out.append(self.eol)
|
|
360
|
+
pos = 0
|
|
361
|
+
else:
|
|
362
|
+
out.append(self.eol + ind.text)
|
|
363
|
+
pos = ind.width
|
|
364
|
+
else: # pragma: no cover - a programming error
|
|
365
|
+
raise TypeError(f"not a document: {node!r}")
|
|
366
|
+
|
|
367
|
+
if not cmds and line_suffix:
|
|
368
|
+
for entry in reversed(line_suffix):
|
|
369
|
+
cmds.append(entry)
|
|
370
|
+
line_suffix = []
|
|
371
|
+
_trim(out)
|
|
372
|
+
return "".join(out)
|
|
373
|
+
|
|
374
|
+
def _fits(
|
|
375
|
+
self,
|
|
376
|
+
next_cmd: tuple[_Indentation, int, Doc],
|
|
377
|
+
rest: list[tuple[_Indentation, int, Doc]],
|
|
378
|
+
width: int,
|
|
379
|
+
group_modes: dict[object, int],
|
|
380
|
+
must_be_flat: bool = False,
|
|
381
|
+
) -> bool:
|
|
382
|
+
rest_index = len(rest)
|
|
383
|
+
cmds: list[tuple[int, Doc]] = [(next_cmd[1], next_cmd[2])]
|
|
384
|
+
while width >= 0:
|
|
385
|
+
if not cmds:
|
|
386
|
+
if rest_index == 0:
|
|
387
|
+
return True
|
|
388
|
+
rest_index -= 1
|
|
389
|
+
entry = rest[rest_index]
|
|
390
|
+
cmds.append((entry[1], entry[2]))
|
|
391
|
+
continue
|
|
392
|
+
mode, node = cmds.pop()
|
|
393
|
+
if isinstance(node, str):
|
|
394
|
+
newline = node.find("\n")
|
|
395
|
+
if newline >= 0:
|
|
396
|
+
# A multi-line token: only its first line has to fit.
|
|
397
|
+
return width - newline >= 0
|
|
398
|
+
width -= len(node)
|
|
399
|
+
elif isinstance(node, list):
|
|
400
|
+
for child in reversed(node):
|
|
401
|
+
cmds.append((mode, child))
|
|
402
|
+
elif isinstance(node, Fill):
|
|
403
|
+
for child in reversed(node.parts):
|
|
404
|
+
cmds.append((mode, child))
|
|
405
|
+
elif isinstance(node, LineSuffix):
|
|
406
|
+
# Printed at the end of the line, so it takes no width here.
|
|
407
|
+
continue
|
|
408
|
+
elif isinstance(node, (Indent, Align, IndentIfBreak)):
|
|
409
|
+
cmds.append((mode, node.contents))
|
|
410
|
+
elif isinstance(node, Group):
|
|
411
|
+
if must_be_flat and node.should_break:
|
|
412
|
+
return False
|
|
413
|
+
group_mode = _MODE_BREAK if node.should_break else mode
|
|
414
|
+
contents = node.contents
|
|
415
|
+
if node.expanded_states is not None and group_mode == _MODE_BREAK:
|
|
416
|
+
contents = node.expanded_states[-1]
|
|
417
|
+
cmds.append((group_mode, contents))
|
|
418
|
+
elif isinstance(node, IfBreak):
|
|
419
|
+
group_mode = group_modes.get(node.group_id, _MODE_FLAT) if node.group_id is not None else mode
|
|
420
|
+
contents = node.break_contents if group_mode == _MODE_BREAK else node.flat_contents
|
|
421
|
+
cmds.append((mode, contents))
|
|
422
|
+
elif isinstance(node, Line):
|
|
423
|
+
if mode == _MODE_BREAK or node.hard:
|
|
424
|
+
return True
|
|
425
|
+
if not node.soft:
|
|
426
|
+
width -= 1
|
|
427
|
+
elif isinstance(node, (Wrapped, EnsureLine)):
|
|
428
|
+
return True
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def _print_fill(
|
|
432
|
+
self,
|
|
433
|
+
ind: _Indentation,
|
|
434
|
+
mode: int,
|
|
435
|
+
node: Fill,
|
|
436
|
+
cmds: list[tuple[_Indentation, int, Doc]],
|
|
437
|
+
pos: int,
|
|
438
|
+
line_suffix: list[tuple[_Indentation, int, Doc]],
|
|
439
|
+
group_modes: dict[object, int],
|
|
440
|
+
) -> None:
|
|
441
|
+
parts = node.parts
|
|
442
|
+
if not parts:
|
|
443
|
+
return
|
|
444
|
+
rem = self.width - pos
|
|
445
|
+
content = parts[0]
|
|
446
|
+
content_flat = (ind, _MODE_FLAT, content)
|
|
447
|
+
content_fits = self._fits(content_flat, [], rem, group_modes, True)
|
|
448
|
+
if len(parts) == 1:
|
|
449
|
+
cmds.append(content_flat if content_fits else (ind, _MODE_BREAK, content))
|
|
450
|
+
return
|
|
451
|
+
whitespace = parts[1]
|
|
452
|
+
if len(parts) == 2:
|
|
453
|
+
if content_fits:
|
|
454
|
+
cmds.append((ind, _MODE_FLAT, whitespace))
|
|
455
|
+
cmds.append(content_flat)
|
|
456
|
+
else:
|
|
457
|
+
cmds.append((ind, _MODE_BREAK, whitespace))
|
|
458
|
+
cmds.append((ind, _MODE_BREAK, content))
|
|
459
|
+
return
|
|
460
|
+
remaining = Fill(parts[2:])
|
|
461
|
+
second = parts[2]
|
|
462
|
+
pair_flat = (ind, _MODE_FLAT, [content, whitespace, second])
|
|
463
|
+
pair_fits = self._fits(pair_flat, [], rem, group_modes, True)
|
|
464
|
+
cmds.append((ind, mode, remaining))
|
|
465
|
+
if pair_fits:
|
|
466
|
+
cmds.append((ind, _MODE_FLAT, whitespace))
|
|
467
|
+
cmds.append(content_flat)
|
|
468
|
+
elif content_fits:
|
|
469
|
+
cmds.append((ind, _MODE_BREAK, whitespace))
|
|
470
|
+
cmds.append(content_flat)
|
|
471
|
+
else:
|
|
472
|
+
cmds.append((ind, _MODE_BREAK, whitespace))
|
|
473
|
+
cmds.append((ind, _MODE_BREAK, content))
|
|
474
|
+
|
|
475
|
+
def _print_wrapped(self, ind: _Indentation, node: Wrapped, out: list[str], pos: int) -> int:
|
|
476
|
+
texts: list[str]
|
|
477
|
+
if node.lines is not None and all(
|
|
478
|
+
(pos if i == 0 else ind.width) + len(line) <= self.width for i, line in enumerate(node.lines)
|
|
479
|
+
):
|
|
480
|
+
texts = node.lines
|
|
481
|
+
else:
|
|
482
|
+
# Greedy word wrap: as many words per line as fit, at least one.
|
|
483
|
+
# The first line starts where the printer is; later ones at the indent.
|
|
484
|
+
texts = [
|
|
485
|
+
(node.prefix + " ".join(words)).rstrip()
|
|
486
|
+
for words in wrap_words(
|
|
487
|
+
node.words,
|
|
488
|
+
first=self.width - pos - len(node.prefix),
|
|
489
|
+
rest=self.width - ind.width - len(node.prefix),
|
|
490
|
+
)
|
|
491
|
+
]
|
|
492
|
+
for i, text in enumerate(texts):
|
|
493
|
+
if i:
|
|
494
|
+
out.append(self.eol + ind.text)
|
|
495
|
+
pos = ind.width
|
|
496
|
+
out.append(text)
|
|
497
|
+
pos += len(text)
|
|
498
|
+
return pos
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def wrap_words(words: Sequence[str], first: int, rest: int) -> list[list[str]]:
|
|
502
|
+
"""Split words into lines no longer than the widths given, where possible."""
|
|
503
|
+
lines: list[list[str]] = [[]]
|
|
504
|
+
available = first
|
|
505
|
+
length = 0
|
|
506
|
+
for word in words:
|
|
507
|
+
current = lines[-1]
|
|
508
|
+
extra = len(word) + (1 if current else 0)
|
|
509
|
+
if current and length + extra > available:
|
|
510
|
+
lines.append([word])
|
|
511
|
+
length = len(word)
|
|
512
|
+
available = rest
|
|
513
|
+
else:
|
|
514
|
+
current.append(word)
|
|
515
|
+
length += extra
|
|
516
|
+
return lines
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _without_leading_space(doc: Doc) -> Doc:
|
|
520
|
+
"""A trailing comment's document, ``[" ", text]``, without its separating space."""
|
|
521
|
+
if isinstance(doc, list) and doc and doc[0] == " ":
|
|
522
|
+
return doc[1:]
|
|
523
|
+
return doc
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _trim(out: list[str]) -> None:
|
|
527
|
+
"""Remove trailing spaces and tabs from the end of the output."""
|
|
528
|
+
while out:
|
|
529
|
+
trimmed = out[-1].rstrip(" \t")
|
|
530
|
+
if trimmed:
|
|
531
|
+
out[-1] = trimmed
|
|
532
|
+
return
|
|
533
|
+
out.pop()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def print_doc(doc: Doc, width: int, indent_unit: str = " ", indent_width: int = 4, eol: str = "\n") -> str:
|
|
537
|
+
return Printer(width, indent_unit, indent_width, eol).print(doc)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The formatting pipeline: parse, rewrite, print, then verify.
|
|
2
|
+
|
|
3
|
+
``format_source`` refuses input that does not parse (``CSharpSyntaxError``)
|
|
4
|
+
and raises ``FormattingError`` rather than return output that the verifier
|
|
5
|
+
finds different in meaning from the input. Formatting runs in a worker
|
|
6
|
+
thread with a large stack, so a deeply nested file fails cleanly instead of
|
|
7
|
+
crashing the interpreter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import TypeVar
|
|
16
|
+
|
|
17
|
+
from prettycsharp.parsing import CSharpSyntaxError
|
|
18
|
+
from prettycsharp.printer import print_file
|
|
19
|
+
from prettycsharp.rewrites import rewrite
|
|
20
|
+
from prettycsharp.settings import Settings
|
|
21
|
+
from prettycsharp.syntax import build
|
|
22
|
+
from prettycsharp.verify import VerificationError, canonical, comment_words
|
|
23
|
+
|
|
24
|
+
LATEST = 99.0
|
|
25
|
+
# 255 MiB: Windows refuses a thread stack of 256 MiB or more.
|
|
26
|
+
_STACK_SIZE = 255 * 1024 * 1024
|
|
27
|
+
_RECURSION_LIMIT = 50_000
|
|
28
|
+
_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FormattingError(Exception):
|
|
34
|
+
"""The formatter could not produce output it can stand behind."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def lang_version_of(settings: Settings, detected: float | None = None) -> float:
|
|
38
|
+
if settings.lang_version == "auto":
|
|
39
|
+
return detected if detected is not None else LATEST
|
|
40
|
+
if settings.lang_version == "latest":
|
|
41
|
+
return LATEST
|
|
42
|
+
return float(settings.lang_version)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_source(source: str, settings: Settings | None = None, *, lang_version: float | None = None) -> str:
|
|
46
|
+
"""Return ``source`` formatted.
|
|
47
|
+
|
|
48
|
+
Raises CSharpSyntaxError when the source does not parse, and
|
|
49
|
+
FormattingError when the result fails the formatter's own check.
|
|
50
|
+
``lang_version`` is the project's C# version, used when the settings say
|
|
51
|
+
``auto``; without it the latest version is assumed.
|
|
52
|
+
"""
|
|
53
|
+
chosen = settings or Settings()
|
|
54
|
+
version = lang_version_of(chosen, lang_version)
|
|
55
|
+
return _run_with_big_stack(lambda: _format(source, chosen, version))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _format(source: str, settings: Settings, lang_version: float) -> str:
|
|
59
|
+
file = build(source)
|
|
60
|
+
expected_tree = canonical(file.root)
|
|
61
|
+
expected_comments = comment_words(file)
|
|
62
|
+
rewrite(file, settings, lang_version)
|
|
63
|
+
text = print_file(file, settings, lang_version)
|
|
64
|
+
try:
|
|
65
|
+
result = build(text)
|
|
66
|
+
except CSharpSyntaxError as error:
|
|
67
|
+
raise FormattingError(f"the formatted code does not parse ({error})") from None
|
|
68
|
+
if canonical(result.root) != expected_tree or comment_words(result) != expected_comments:
|
|
69
|
+
try:
|
|
70
|
+
from prettycsharp.verify import verify
|
|
71
|
+
|
|
72
|
+
original = build(source)
|
|
73
|
+
verify(original, result)
|
|
74
|
+
except VerificationError as error:
|
|
75
|
+
raise FormattingError(str(error)) from None
|
|
76
|
+
raise FormattingError("the formatted code differs from the source")
|
|
77
|
+
return text
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _run_with_big_stack(work: Callable[[], T]) -> T:
|
|
81
|
+
result: list[T] = []
|
|
82
|
+
failure: list[BaseException] = []
|
|
83
|
+
|
|
84
|
+
def target() -> None:
|
|
85
|
+
try:
|
|
86
|
+
result.append(work())
|
|
87
|
+
except RecursionError:
|
|
88
|
+
failure.append(FormattingError("the file is nested too deeply to format"))
|
|
89
|
+
except BaseException as error: # noqa: BLE001 - re-raised in the caller's thread
|
|
90
|
+
failure.append(error)
|
|
91
|
+
|
|
92
|
+
with _lock:
|
|
93
|
+
old_limit = sys.getrecursionlimit()
|
|
94
|
+
old_size = threading.stack_size()
|
|
95
|
+
try:
|
|
96
|
+
threading.stack_size(_STACK_SIZE)
|
|
97
|
+
sys.setrecursionlimit(max(old_limit, _RECURSION_LIMIT))
|
|
98
|
+
worker = threading.Thread(target=target, name="prettycsharp-format")
|
|
99
|
+
worker.start()
|
|
100
|
+
worker.join()
|
|
101
|
+
finally:
|
|
102
|
+
threading.stack_size(old_size)
|
|
103
|
+
sys.setrecursionlimit(old_limit)
|
|
104
|
+
if failure:
|
|
105
|
+
raise failure[0]
|
|
106
|
+
return result[0]
|