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.
@@ -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"]