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
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""The printer's shared machinery: dispatch, tokens, comments and lists.
|
|
2
|
+
|
|
3
|
+
Each syntax node type has a handler method named ``p_<type>`` in one of the
|
|
4
|
+
mixins (declarations, statements, expressions). A node without a handler is
|
|
5
|
+
printed by ``generic``, which joins its children with the spacing the source
|
|
6
|
+
had. Every token goes through ``tok``, which prints the comments attached to
|
|
7
|
+
it, so no comment is dropped by a handler that rearranges tokens.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
|
|
16
|
+
from prettycsharp.doc import (
|
|
17
|
+
BREAK_PARENT,
|
|
18
|
+
ENSURE_LINE,
|
|
19
|
+
HARDLINE,
|
|
20
|
+
LINE,
|
|
21
|
+
SOFTLINE,
|
|
22
|
+
TRIM_INDENT,
|
|
23
|
+
Doc,
|
|
24
|
+
Group,
|
|
25
|
+
LineSuffix,
|
|
26
|
+
Wrapped,
|
|
27
|
+
group,
|
|
28
|
+
indent,
|
|
29
|
+
join,
|
|
30
|
+
)
|
|
31
|
+
from prettycsharp.settings import Settings
|
|
32
|
+
from prettycsharp.syntax import DIRECTIVE_TYPES, Comment, Node, SourceFile
|
|
33
|
+
|
|
34
|
+
Handler = Callable[[Node], Doc]
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
_WORD = re.compile(r"[A-Za-z0-9_@$\u0080-]")
|
|
38
|
+
# Operator characters that merge into a different token when written
|
|
39
|
+
# together, so printing them adjacent needs a space between.
|
|
40
|
+
_MERGING_PAIRS = {
|
|
41
|
+
("+", "+"),
|
|
42
|
+
("-", "-"),
|
|
43
|
+
("&", "&"),
|
|
44
|
+
("|", "|"),
|
|
45
|
+
("=", "="),
|
|
46
|
+
("<", "<"),
|
|
47
|
+
(">", ">"),
|
|
48
|
+
("<", "="),
|
|
49
|
+
(">", "="),
|
|
50
|
+
("!", "="),
|
|
51
|
+
("=", ">"),
|
|
52
|
+
("?", "?"),
|
|
53
|
+
("/", "/"),
|
|
54
|
+
("/", "*"),
|
|
55
|
+
("*", "/"),
|
|
56
|
+
("-", ">"),
|
|
57
|
+
(":", ":"),
|
|
58
|
+
(".", "."),
|
|
59
|
+
("+", "="),
|
|
60
|
+
("-", "="),
|
|
61
|
+
("*", "="),
|
|
62
|
+
("/", "="),
|
|
63
|
+
("%", "="),
|
|
64
|
+
("&", "="),
|
|
65
|
+
("|", "="),
|
|
66
|
+
("^", "="),
|
|
67
|
+
("?", "."),
|
|
68
|
+
("?", "["),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_URL = re.compile(r"\w+://")
|
|
72
|
+
_LIST_ITEM = re.compile(r"^(?:[-*+]\s|\d+[.)]\s)")
|
|
73
|
+
|
|
74
|
+
_INDENTING = frozenset(
|
|
75
|
+
{"declaration_list", "block", "accessor_list", "enum_member_declaration_list"}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
IGNORE = "prettycsharp-ignore"
|
|
79
|
+
IGNORE_START = "prettycsharp-ignore-start"
|
|
80
|
+
IGNORE_END = "prettycsharp-ignore-end"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def comment_body(comment: Comment) -> str:
|
|
84
|
+
return comment.text[2:].strip()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Core:
|
|
88
|
+
def __init__(self, settings: Settings, file: SourceFile, lang_version: float) -> None:
|
|
89
|
+
self.settings = settings
|
|
90
|
+
self.file = file
|
|
91
|
+
self.lang = lang_version
|
|
92
|
+
self._blank_stack: list[Callable[[Node, Node], bool]] = []
|
|
93
|
+
self._handlers: dict[str, Handler] = {}
|
|
94
|
+
for name in dir(self):
|
|
95
|
+
if name.startswith("p_"):
|
|
96
|
+
self._handlers[name[2:]] = getattr(self, name)
|
|
97
|
+
|
|
98
|
+
# Dispatch -----------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def print(self, node: Node) -> Doc:
|
|
101
|
+
if node.verbatim:
|
|
102
|
+
return self.verbatim(node)
|
|
103
|
+
if node.text is not None:
|
|
104
|
+
return self.tok(node)
|
|
105
|
+
handler = self._handlers.get(node.type)
|
|
106
|
+
if handler is None:
|
|
107
|
+
return self.generic(node)
|
|
108
|
+
return handler(node)
|
|
109
|
+
|
|
110
|
+
# Tokens and comments ------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def tok(self, t: Node | None, *, leading: bool = True, trailing: bool = True) -> Doc:
|
|
113
|
+
if t is None:
|
|
114
|
+
return ""
|
|
115
|
+
if t.text is None:
|
|
116
|
+
return self.print(t)
|
|
117
|
+
parts: list[Doc] = []
|
|
118
|
+
if leading and t.leading:
|
|
119
|
+
parts.append(self.leading_comments(t))
|
|
120
|
+
parts.append(self.token_text(t))
|
|
121
|
+
if trailing and t.trailing:
|
|
122
|
+
parts.append(self.trailing_comments(t.trailing))
|
|
123
|
+
return parts if len(parts) > 1 else parts[0]
|
|
124
|
+
|
|
125
|
+
def token_text(self, t: Node) -> Doc:
|
|
126
|
+
text = t.text or ""
|
|
127
|
+
if "\n" in text or "\r" in text:
|
|
128
|
+
return self.multiline_token(text)
|
|
129
|
+
return text
|
|
130
|
+
|
|
131
|
+
def multiline_token(self, text: str) -> Doc:
|
|
132
|
+
# A multi-line string token is content: printed exactly, its line
|
|
133
|
+
# breaks kept as written, its continuation lines not reindented.
|
|
134
|
+
return [text, BREAK_PARENT]
|
|
135
|
+
|
|
136
|
+
def leading_comments(self, t: Node, *, first_blank_allowed: bool = False) -> Doc:
|
|
137
|
+
"""The comments before a token, each on its own line, then the token's line."""
|
|
138
|
+
# Each comment stood on its own line: start one if code is already on this line.
|
|
139
|
+
parts: list[Doc] = [ENSURE_LINE]
|
|
140
|
+
comments = t.leading
|
|
141
|
+
i = 0
|
|
142
|
+
while i < len(comments):
|
|
143
|
+
comment = comments[i]
|
|
144
|
+
if i > 0 or first_blank_allowed:
|
|
145
|
+
if comment.newlines_before >= 2 and self.settings.max_blank_lines > 0:
|
|
146
|
+
parts.append(HARDLINE)
|
|
147
|
+
# A run of line comments on consecutive lines is one block, so a
|
|
148
|
+
# paragraph can be rewrapped as a whole.
|
|
149
|
+
run = [comment]
|
|
150
|
+
if comment.is_line and not comment.is_doc:
|
|
151
|
+
while (
|
|
152
|
+
i + len(run) < len(comments)
|
|
153
|
+
and comments[i + len(run)].is_line
|
|
154
|
+
and not comments[i + len(run)].is_doc
|
|
155
|
+
and comments[i + len(run)].newlines_before == 1
|
|
156
|
+
):
|
|
157
|
+
run.append(comments[i + len(run)])
|
|
158
|
+
if len(run) > 1 or comment.is_line:
|
|
159
|
+
parts.append(self.line_comment_block(run))
|
|
160
|
+
parts.append(HARDLINE)
|
|
161
|
+
else:
|
|
162
|
+
parts.append(self.block_comment(comment))
|
|
163
|
+
parts.append(HARDLINE if comment.newline_after else " ")
|
|
164
|
+
i += len(run)
|
|
165
|
+
if t.newlines_before >= 2 and comments and self.settings.max_blank_lines > 0 and parts:
|
|
166
|
+
if parts[-1] is HARDLINE:
|
|
167
|
+
parts.append(HARDLINE)
|
|
168
|
+
return parts
|
|
169
|
+
|
|
170
|
+
def trailing_comments(self, comments: Sequence[Comment], *, break_parent: bool = True) -> Doc:
|
|
171
|
+
"""Comments after a token on its line.
|
|
172
|
+
|
|
173
|
+
A line comment waits for the end of the line, and the printer puts
|
|
174
|
+
any code after it on the next line. ``break_parent`` also breaks the
|
|
175
|
+
enclosing groups, so a comment inside a list starts a layout of one
|
|
176
|
+
item per line; a comment after a group's closing token does not need it.
|
|
177
|
+
"""
|
|
178
|
+
parts: list[Doc] = []
|
|
179
|
+
for comment in comments:
|
|
180
|
+
if comment.is_line:
|
|
181
|
+
parts.append(LineSuffix([" ", self.normalize_line_comment(comment.text)]))
|
|
182
|
+
if break_parent:
|
|
183
|
+
parts.append(BREAK_PARENT)
|
|
184
|
+
else:
|
|
185
|
+
parts.append([" ", self.block_comment(comment)])
|
|
186
|
+
return parts
|
|
187
|
+
|
|
188
|
+
def normalize_line_comment(self, text: str) -> str:
|
|
189
|
+
text = text.rstrip()
|
|
190
|
+
if self.settings.comment_space and len(text) > 2 and text[2].isalnum():
|
|
191
|
+
return "// " + text[2:]
|
|
192
|
+
return text
|
|
193
|
+
|
|
194
|
+
def block_comment(self, comment: Comment) -> Doc:
|
|
195
|
+
lines = re.split(r"\r\n|\r|\n", comment.text)
|
|
196
|
+
if len(lines) == 1:
|
|
197
|
+
return comment.text
|
|
198
|
+
rest = lines[1:]
|
|
199
|
+
stars = all(line.strip().startswith("*") for line in rest if line.strip())
|
|
200
|
+
if stars:
|
|
201
|
+
body = [" " + line.strip() if line.strip() else "" for line in rest]
|
|
202
|
+
else:
|
|
203
|
+
margin = min((len(line) - len(line.lstrip()) for line in rest if line.strip()), default=0)
|
|
204
|
+
body = [line[margin:].rstrip() for line in rest]
|
|
205
|
+
parts: list[Doc] = [lines[0].rstrip()]
|
|
206
|
+
for line in body:
|
|
207
|
+
parts.append(HARDLINE)
|
|
208
|
+
parts.append(line)
|
|
209
|
+
return parts
|
|
210
|
+
|
|
211
|
+
def line_comment_block(self, run: Sequence[Comment]) -> Doc:
|
|
212
|
+
"""Consecutive // comments: plain lines, with long paragraphs rewrapped."""
|
|
213
|
+
texts = [self.normalize_line_comment(c.text) for c in run]
|
|
214
|
+
if not self.settings.comment_wrap:
|
|
215
|
+
return join(HARDLINE, texts)
|
|
216
|
+
segments: list[Doc] = []
|
|
217
|
+
paragraph: list[str] = []
|
|
218
|
+
|
|
219
|
+
def flush() -> None:
|
|
220
|
+
if paragraph:
|
|
221
|
+
segments.append(_paragraph(list(paragraph)))
|
|
222
|
+
paragraph.clear()
|
|
223
|
+
|
|
224
|
+
for text in texts:
|
|
225
|
+
if _reflowable(text, len(texts)):
|
|
226
|
+
paragraph.append(text)
|
|
227
|
+
else:
|
|
228
|
+
flush()
|
|
229
|
+
segments.append(text)
|
|
230
|
+
flush()
|
|
231
|
+
return join(HARDLINE, segments)
|
|
232
|
+
|
|
233
|
+
# Verbatim ----------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def verbatim(self, node: Node) -> Doc:
|
|
236
|
+
first = node.first_token()
|
|
237
|
+
last = node.last_token()
|
|
238
|
+
if first is None or last is None or node.start < 0:
|
|
239
|
+
return self.generic(node)
|
|
240
|
+
text = self.file.text(first.start, last.end)
|
|
241
|
+
parts: list[Doc] = []
|
|
242
|
+
if first.leading:
|
|
243
|
+
parts.append(self.leading_comments(first))
|
|
244
|
+
# Exactly as written, line breaks and trailing spaces included: they
|
|
245
|
+
# may be inside a multi-line string.
|
|
246
|
+
parts.append(self.multiline_token(text) if ("\n" in text or "\r" in text) else text)
|
|
247
|
+
if last.trailing:
|
|
248
|
+
parts.append(self.trailing_comments(last.trailing))
|
|
249
|
+
return parts
|
|
250
|
+
|
|
251
|
+
# Generic printing ---------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def generic(self, node: Node) -> Doc:
|
|
254
|
+
"""Children in order, separated by a space wherever the source had one."""
|
|
255
|
+
parts: list[Doc] = []
|
|
256
|
+
previous: Node | None = None
|
|
257
|
+
for child in node.children:
|
|
258
|
+
if previous is not None and self.space_between(previous, child):
|
|
259
|
+
parts.append(" ")
|
|
260
|
+
parts.append(self.print(child))
|
|
261
|
+
previous = child
|
|
262
|
+
return parts
|
|
263
|
+
|
|
264
|
+
def space_between(self, left: Node, right: Node) -> bool:
|
|
265
|
+
a = left.last_token()
|
|
266
|
+
b = right.first_token()
|
|
267
|
+
if a is None or b is None:
|
|
268
|
+
return False
|
|
269
|
+
if a.end >= 0 and b.start >= 0 and b.start >= a.end:
|
|
270
|
+
if b.start > a.end or a.trailing or b.leading:
|
|
271
|
+
return True
|
|
272
|
+
return self.would_merge(a.text or "", b.text or "")
|
|
273
|
+
return self.default_space(a.text or "", b.text or "")
|
|
274
|
+
|
|
275
|
+
def default_space(self, left: str, right: str) -> bool:
|
|
276
|
+
if not left or not right:
|
|
277
|
+
return False
|
|
278
|
+
if right in (",", ";", ")", "]", ".", "?.") or left in ("(", "[", ".", "?."):
|
|
279
|
+
return False
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def would_merge(left: str, right: str) -> bool:
|
|
284
|
+
if not left or not right:
|
|
285
|
+
return False
|
|
286
|
+
a, b = left[-1], right[0]
|
|
287
|
+
if _WORD.match(a) and _WORD.match(b):
|
|
288
|
+
return True
|
|
289
|
+
return (a, b) in _MERGING_PAIRS
|
|
290
|
+
|
|
291
|
+
def join_tokens(self, docs: Sequence[tuple[Node, Doc]]) -> Doc:
|
|
292
|
+
"""Docs for adjacent nodes, with a space only where tokens would merge."""
|
|
293
|
+
parts: list[Doc] = []
|
|
294
|
+
previous: Node | None = None
|
|
295
|
+
for node, doc in docs:
|
|
296
|
+
if previous is not None:
|
|
297
|
+
a = previous.last_token()
|
|
298
|
+
b = node.first_token()
|
|
299
|
+
if a is not None and b is not None and self.would_merge(a.text or "", b.text or ""):
|
|
300
|
+
parts.append(" ")
|
|
301
|
+
parts.append(doc)
|
|
302
|
+
previous = node
|
|
303
|
+
return parts
|
|
304
|
+
|
|
305
|
+
def spaced(self, nodes: Iterable[Node]) -> Doc:
|
|
306
|
+
"""Nodes separated by single spaces."""
|
|
307
|
+
return join(" ", [self.print(n) for n in nodes])
|
|
308
|
+
|
|
309
|
+
def indent_depth(self, node: Node) -> int:
|
|
310
|
+
"""How many indentation levels the node will print at, as far as its ancestors tell."""
|
|
311
|
+
depth = 0
|
|
312
|
+
parent = node.parent
|
|
313
|
+
while parent is not None:
|
|
314
|
+
if parent.type in _INDENTING:
|
|
315
|
+
depth += 1
|
|
316
|
+
elif parent.type == "switch_section" or (parent.type == "switch_body" and self.settings.switch_case_indent):
|
|
317
|
+
depth += 1
|
|
318
|
+
parent = parent.parent
|
|
319
|
+
return depth
|
|
320
|
+
|
|
321
|
+
# Lists --------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def has_blank_before(self, node: Node) -> bool:
|
|
324
|
+
first = node.first_token()
|
|
325
|
+
if first is None or self.settings.max_blank_lines == 0:
|
|
326
|
+
return False
|
|
327
|
+
if first.leading:
|
|
328
|
+
return first.leading[0].newlines_before >= 2
|
|
329
|
+
return first.newlines_before >= 2
|
|
330
|
+
|
|
331
|
+
def has_comments_before(self, node: Node) -> bool:
|
|
332
|
+
first = node.first_token()
|
|
333
|
+
return first is not None and bool(first.leading)
|
|
334
|
+
|
|
335
|
+
def ignore_marker(self, node: Node) -> str | None:
|
|
336
|
+
first = node.first_token()
|
|
337
|
+
if first is None:
|
|
338
|
+
return None
|
|
339
|
+
for comment in first.leading:
|
|
340
|
+
if comment.is_line and not comment.is_doc:
|
|
341
|
+
body = comment_body(comment)
|
|
342
|
+
if body in (IGNORE, IGNORE_START, IGNORE_END):
|
|
343
|
+
return body
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
def is_column_zero_directive(self, node: Node) -> bool:
|
|
347
|
+
if node.type not in DIRECTIVE_TYPES or node.type in ("preproc_region", "preproc_endregion"):
|
|
348
|
+
return False
|
|
349
|
+
first = node.first_token()
|
|
350
|
+
return first is not None and first.column == 0 and not first.leading
|
|
351
|
+
|
|
352
|
+
def print_list(
|
|
353
|
+
self,
|
|
354
|
+
items: Sequence[Node],
|
|
355
|
+
blank: Callable[[Node, Node], bool],
|
|
356
|
+
print_item: Callable[[Node], Doc] | None = None,
|
|
357
|
+
aligned: dict[int, Doc] | None = None,
|
|
358
|
+
) -> Doc:
|
|
359
|
+
"""Items one per line; ``blank(previous, item)`` decides blank lines.
|
|
360
|
+
|
|
361
|
+
Ignored regions are printed as written, and preprocessor directives
|
|
362
|
+
take a line of their own.
|
|
363
|
+
"""
|
|
364
|
+
printer = print_item or self.print
|
|
365
|
+
if self.settings.regions == "remove":
|
|
366
|
+
items = [i for i in items if i.type not in ("preproc_region", "preproc_endregion") or self.has_comments_before(i)]
|
|
367
|
+
self._blank_stack.append(blank)
|
|
368
|
+
try:
|
|
369
|
+
return self._print_list(items, blank, printer, aligned)
|
|
370
|
+
finally:
|
|
371
|
+
self._blank_stack.pop()
|
|
372
|
+
|
|
373
|
+
def _print_list(
|
|
374
|
+
self,
|
|
375
|
+
items: Sequence[Node],
|
|
376
|
+
blank: Callable[[Node, Node], bool],
|
|
377
|
+
printer: Callable[[Node], Doc],
|
|
378
|
+
aligned: dict[int, Doc] | None,
|
|
379
|
+
) -> Doc:
|
|
380
|
+
parts: list[Doc] = []
|
|
381
|
+
previous: Node | None = None
|
|
382
|
+
i = 0
|
|
383
|
+
while i < len(items):
|
|
384
|
+
item = items[i]
|
|
385
|
+
marker = self.ignore_marker(item)
|
|
386
|
+
if previous is not None:
|
|
387
|
+
parts.append([HARDLINE, HARDLINE] if blank(previous, item) else HARDLINE)
|
|
388
|
+
if self.is_column_zero_directive(item):
|
|
389
|
+
# A directive written at column 0 stays there.
|
|
390
|
+
parts.append(TRIM_INDENT)
|
|
391
|
+
if marker == IGNORE_START:
|
|
392
|
+
end = i
|
|
393
|
+
while end + 1 < len(items) and self.ignore_marker(items[end + 1]) != IGNORE_END:
|
|
394
|
+
end += 1
|
|
395
|
+
parts.append(self.verbatim_range(items[i : end + 1]))
|
|
396
|
+
previous = items[end]
|
|
397
|
+
i = end + 1
|
|
398
|
+
continue
|
|
399
|
+
if marker == IGNORE:
|
|
400
|
+
parts.append(self.verbatim(item))
|
|
401
|
+
elif aligned is not None and id(item) in aligned:
|
|
402
|
+
parts.append(aligned[id(item)])
|
|
403
|
+
else:
|
|
404
|
+
parts.append(printer(item))
|
|
405
|
+
previous = item
|
|
406
|
+
i += 1
|
|
407
|
+
return parts
|
|
408
|
+
|
|
409
|
+
def verbatim_range(self, items: Sequence[Node]) -> Doc:
|
|
410
|
+
first = items[0].first_token()
|
|
411
|
+
last = items[-1].last_token()
|
|
412
|
+
if first is None or last is None or first.start < 0 or last.end < 0:
|
|
413
|
+
return join(HARDLINE, [self.print(item) for item in items])
|
|
414
|
+
holder = Node(type="verbatim_range", start=first.start, end=last.end)
|
|
415
|
+
holder.children = list(items)
|
|
416
|
+
return self.verbatim(holder)
|
|
417
|
+
|
|
418
|
+
# Shared layout helpers -----------------------------------------------
|
|
419
|
+
|
|
420
|
+
def braced_list(self, open_tok: Node, items: Sequence[Doc], close_tok: Node, *, force_break: bool) -> Doc:
|
|
421
|
+
"""``{`` items ``}`` with each item on its own line when broken."""
|
|
422
|
+
inner: list[Doc] = []
|
|
423
|
+
if items:
|
|
424
|
+
inner.append(indent([LINE, join([",", LINE], list(items))]))
|
|
425
|
+
inner.append(self.close_comments(close_tok))
|
|
426
|
+
inner.append(LINE)
|
|
427
|
+
return group([self.tok(open_tok), inner, self.tok(close_tok, leading=False)], should_break=force_break)
|
|
428
|
+
|
|
429
|
+
def split_leading(self, node: Node, build: Callable[[], T]) -> tuple[Doc, T]:
|
|
430
|
+
"""(the comments above the node, what ``build`` makes of the node without them)."""
|
|
431
|
+
first = node.first_token()
|
|
432
|
+
if first is None or not first.leading:
|
|
433
|
+
return "", build()
|
|
434
|
+
saved = first.leading
|
|
435
|
+
first.leading = []
|
|
436
|
+
try:
|
|
437
|
+
built = build()
|
|
438
|
+
finally:
|
|
439
|
+
first.leading = saved
|
|
440
|
+
return self.leading_comments(first), built
|
|
441
|
+
|
|
442
|
+
def split_trailing(self, node: Node, build: Callable[[], T]) -> tuple[Doc, T]:
|
|
443
|
+
"""(the comments after the node's last token, what ``build`` makes of the node without them)."""
|
|
444
|
+
last = node.last_token()
|
|
445
|
+
if last is None or not last.trailing:
|
|
446
|
+
return "", build()
|
|
447
|
+
saved = last.trailing
|
|
448
|
+
last.trailing = []
|
|
449
|
+
try:
|
|
450
|
+
built = build()
|
|
451
|
+
finally:
|
|
452
|
+
last.trailing = saved
|
|
453
|
+
return self.trailing_comments(saved), built
|
|
454
|
+
|
|
455
|
+
def closing(self, close: Node, line: Doc) -> Doc:
|
|
456
|
+
"""``line`` then the closing token, with comments before it on lines of their own first.
|
|
457
|
+
|
|
458
|
+
With comments and no ``line`` (a closing token that hugs the last
|
|
459
|
+
item), a line break still separates the last comment from the token.
|
|
460
|
+
"""
|
|
461
|
+
if close.leading and line == "":
|
|
462
|
+
line = HARDLINE
|
|
463
|
+
return [
|
|
464
|
+
self.close_comments(close),
|
|
465
|
+
line,
|
|
466
|
+
self.tok(close, leading=False, trailing=False),
|
|
467
|
+
self.trailing_comments(close.trailing, break_parent=False),
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
def close_comments(self, close: Node | None) -> Doc:
|
|
471
|
+
"""Comments before a closing token, printed inside the indented block."""
|
|
472
|
+
if close is None or not close.leading:
|
|
473
|
+
return ""
|
|
474
|
+
parts = self.leading_comments(close)
|
|
475
|
+
assert isinstance(parts, list)
|
|
476
|
+
while parts and parts[-1] is HARDLINE:
|
|
477
|
+
parts.pop()
|
|
478
|
+
before: list[Doc] = [HARDLINE]
|
|
479
|
+
if close.leading[0].newlines_before >= 2 and self.settings.max_blank_lines > 0:
|
|
480
|
+
before.append(HARDLINE)
|
|
481
|
+
return indent([*before, parts])
|
|
482
|
+
|
|
483
|
+
def open_brace(self, context: str) -> Doc:
|
|
484
|
+
"""What goes before an opening brace: a line break or a space."""
|
|
485
|
+
style = self.settings.brace_style
|
|
486
|
+
if style == "allman" or (style == "mixed" and context in ("type", "member", "namespace", "accessor")):
|
|
487
|
+
return HARDLINE
|
|
488
|
+
return " "
|
|
489
|
+
|
|
490
|
+
def separated(self, node: Node, separator: str = ",") -> list[list[Node]]:
|
|
491
|
+
"""The children between the first and last token, split at separators."""
|
|
492
|
+
return split(node.children[1:-1], separator)[0]
|
|
493
|
+
|
|
494
|
+
def separators(self, node: Node, separator: str = ",") -> list[Node]:
|
|
495
|
+
return split(node.children[1:-1], separator)[1]
|
|
496
|
+
|
|
497
|
+
def current_blank(self) -> Callable[[Node, Node], bool]:
|
|
498
|
+
"""The blank-line rule of the list being printed, for directives inside it."""
|
|
499
|
+
if self._blank_stack:
|
|
500
|
+
return self._blank_stack[-1]
|
|
501
|
+
return lambda previous, item: self.has_blank_before(item)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def split(nodes: Sequence[Node], separator: str = ",") -> tuple[list[list[Node]], list[Node]]:
|
|
505
|
+
"""Nodes split into items at separator tokens: (items, separators)."""
|
|
506
|
+
items: list[list[Node]] = [[]]
|
|
507
|
+
separators: list[Node] = []
|
|
508
|
+
for node in nodes:
|
|
509
|
+
if node.text == separator and not node.named:
|
|
510
|
+
separators.append(node)
|
|
511
|
+
items.append([])
|
|
512
|
+
else:
|
|
513
|
+
items[-1].append(node)
|
|
514
|
+
if items[-1] == []:
|
|
515
|
+
items.pop()
|
|
516
|
+
return items, separators
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _paragraph(texts: list[str]) -> Doc:
|
|
520
|
+
"""Line comments printed as written unless one is too long, then rewrapped."""
|
|
521
|
+
words = [word for text in texts for word in text[2:].split()]
|
|
522
|
+
return Wrapped("// ", words, list(texts))
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _reflowable(text: str, run_length: int) -> bool:
|
|
526
|
+
body = text[2:]
|
|
527
|
+
stripped = body.strip()
|
|
528
|
+
if not stripped or not body.startswith(" ") or body.startswith(" "):
|
|
529
|
+
return False
|
|
530
|
+
if stripped.endswith((";", "{", "}")) or "=>" in stripped:
|
|
531
|
+
return False
|
|
532
|
+
if _URL.search(stripped) or _LIST_ITEM.match(stripped):
|
|
533
|
+
return False
|
|
534
|
+
if stripped.upper().startswith("TODO") and run_length > 1:
|
|
535
|
+
return False
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
__all__ = ["Core", "Group", "SOFTLINE", "comment_body"]
|