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/settings.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Every formatting setting, its default and the values it accepts.
|
|
2
|
+
|
|
3
|
+
DESIGN.md describes each one. ``Settings.with_values`` validates a mapping
|
|
4
|
+
of setting names to values, as read from a configuration file or the
|
|
5
|
+
command line, and raises ``SettingsError`` naming the first problem.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
# The .NET modifier order (IDE0036's default), with the modifiers C# needs
|
|
15
|
+
# in a fixed place: `ref` just before `partial` or the type keyword, and
|
|
16
|
+
# `partial` last.
|
|
17
|
+
DOTNET_MODIFIER_ORDER = (
|
|
18
|
+
"public",
|
|
19
|
+
"private",
|
|
20
|
+
"protected",
|
|
21
|
+
"internal",
|
|
22
|
+
"file",
|
|
23
|
+
"static",
|
|
24
|
+
"extern",
|
|
25
|
+
"new",
|
|
26
|
+
"virtual",
|
|
27
|
+
"abstract",
|
|
28
|
+
"sealed",
|
|
29
|
+
"override",
|
|
30
|
+
"readonly",
|
|
31
|
+
"unsafe",
|
|
32
|
+
"required",
|
|
33
|
+
"volatile",
|
|
34
|
+
"async",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SettingsError(ValueError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Settings:
|
|
44
|
+
indent_style: str = "space"
|
|
45
|
+
indent_size: int = 4
|
|
46
|
+
max_line_length: int = 120
|
|
47
|
+
end_of_line: str = "preserve"
|
|
48
|
+
charset: str = "preserve"
|
|
49
|
+
insert_final_newline: bool = True
|
|
50
|
+
trim_trailing_whitespace: bool = True
|
|
51
|
+
lang_version: str = "auto"
|
|
52
|
+
|
|
53
|
+
brace_style: str = "allman"
|
|
54
|
+
continuation_keywords: str = "blank_line"
|
|
55
|
+
braces: str = "preserve"
|
|
56
|
+
empty_bodies: str = "inline"
|
|
57
|
+
|
|
58
|
+
member_blank_lines: str = "group_fields"
|
|
59
|
+
blank_lines_around_blocks: bool = True
|
|
60
|
+
max_blank_lines: int = 1
|
|
61
|
+
|
|
62
|
+
argument_wrap: str = "one_per_line"
|
|
63
|
+
chain_break_threshold: int = 2
|
|
64
|
+
expression_body_chain: str = "own_line"
|
|
65
|
+
binary_wrap: str = "before_operator_paren_own_line"
|
|
66
|
+
ternary_wrap: str = "before"
|
|
67
|
+
lambda_body: str = "hug"
|
|
68
|
+
|
|
69
|
+
initializer_break_threshold: int = 2
|
|
70
|
+
simple_lists_inline: bool = True
|
|
71
|
+
trailing_commas: str = "remove"
|
|
72
|
+
|
|
73
|
+
align_switch_arms: bool = True
|
|
74
|
+
align_assignments: bool = True
|
|
75
|
+
align_enum_values: bool = True
|
|
76
|
+
|
|
77
|
+
switch_case_indent: bool = True
|
|
78
|
+
switch_case_blank_lines: bool = True
|
|
79
|
+
|
|
80
|
+
namespace_style: str = "file_scoped_if_supported"
|
|
81
|
+
using_order: str = "alphabetical"
|
|
82
|
+
expression_bodies: str = "prefer"
|
|
83
|
+
attributes: str = "separate"
|
|
84
|
+
modifier_order: tuple[str, ...] | str = "dotnet"
|
|
85
|
+
parentheses: str = "remove_obvious"
|
|
86
|
+
regions: str = "preserve"
|
|
87
|
+
|
|
88
|
+
comment_space: bool = True
|
|
89
|
+
comment_wrap: bool = True
|
|
90
|
+
|
|
91
|
+
respect_gitignore: bool = True
|
|
92
|
+
|
|
93
|
+
def with_values(self, values: Mapping[str, object], source: str = "settings") -> Settings:
|
|
94
|
+
changes: dict[str, object] = {}
|
|
95
|
+
for name, value in values.items():
|
|
96
|
+
key = name.replace("-", "_")
|
|
97
|
+
if key not in _FIELDS:
|
|
98
|
+
raise SettingsError(f"{source}: unknown setting {name!r}")
|
|
99
|
+
changes[key] = _validate(key, value, source)
|
|
100
|
+
return dataclasses.replace(self, **changes) # type: ignore[arg-type]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def width(self) -> int:
|
|
104
|
+
return self.max_line_length if self.max_line_length > 0 else 1_000_000
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def indent_unit(self) -> str:
|
|
108
|
+
return "\t" if self.indent_style == "tab" else " " * self.indent_size
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def modifiers(self) -> tuple[str, ...] | None:
|
|
112
|
+
"""The modifier order to apply, or None to leave modifiers alone."""
|
|
113
|
+
if self.modifier_order == "preserve":
|
|
114
|
+
return None
|
|
115
|
+
if self.modifier_order == "dotnet":
|
|
116
|
+
return DOTNET_MODIFIER_ORDER
|
|
117
|
+
assert isinstance(self.modifier_order, tuple)
|
|
118
|
+
return self.modifier_order
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_CHOICES: dict[str, tuple[str, ...]] = {
|
|
122
|
+
"indent_style": ("space", "tab"),
|
|
123
|
+
"end_of_line": ("preserve", "lf", "crlf", "cr"),
|
|
124
|
+
"charset": ("preserve", "utf-8", "utf-8-bom"),
|
|
125
|
+
"brace_style": ("allman", "kr", "mixed"),
|
|
126
|
+
"continuation_keywords": ("blank_line", "new_line", "same_line"),
|
|
127
|
+
"braces": ("preserve", "always", "single_line_only"),
|
|
128
|
+
"empty_bodies": ("inline", "allman"),
|
|
129
|
+
"member_blank_lines": ("group_fields", "all", "preserve"),
|
|
130
|
+
"argument_wrap": ("one_per_line", "one_per_line_close_on_last", "fill", "align"),
|
|
131
|
+
"expression_body_chain": ("own_line", "hang"),
|
|
132
|
+
"binary_wrap": ("before_operator_paren_own_line", "before_operator", "after_operator"),
|
|
133
|
+
"ternary_wrap": ("before", "after", "always"),
|
|
134
|
+
"lambda_body": ("hug", "argument"),
|
|
135
|
+
"trailing_commas": ("remove", "add", "preserve"),
|
|
136
|
+
"namespace_style": ("file_scoped_if_supported", "file_scoped", "block", "preserve"),
|
|
137
|
+
"using_order": ("alphabetical", "system_first", "system_first_grouped", "preserve"),
|
|
138
|
+
"expression_bodies": ("prefer", "block", "properties_only", "preserve"),
|
|
139
|
+
"attributes": ("separate", "combine", "preserve"),
|
|
140
|
+
"parentheses": ("remove_obvious", "remove_redundant", "preserve"),
|
|
141
|
+
"regions": ("preserve", "remove", "normalize"),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_FIELDS = {f.name: f for f in dataclasses.fields(Settings)}
|
|
145
|
+
|
|
146
|
+
_KNOWN_MODIFIERS = frozenset(DOTNET_MODIFIER_ORDER) | {"const", "partial", "ref", "fixed", "scoped"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _validate(key: str, value: object, source: str) -> object:
|
|
150
|
+
default = getattr(Settings(), key)
|
|
151
|
+
where = f"{source}: {key}"
|
|
152
|
+
if key == "modifier_order":
|
|
153
|
+
if value in ("dotnet", "preserve"):
|
|
154
|
+
return value
|
|
155
|
+
names = value.split(",") if isinstance(value, str) else value
|
|
156
|
+
if not isinstance(names, (list, tuple)) or not names:
|
|
157
|
+
raise SettingsError(f"{where} must be 'dotnet', 'preserve' or a list of modifiers")
|
|
158
|
+
order = tuple(str(name).strip() for name in names)
|
|
159
|
+
unknown = [name for name in order if name not in _KNOWN_MODIFIERS]
|
|
160
|
+
if unknown:
|
|
161
|
+
raise SettingsError(f"{where}: unknown modifier {unknown[0]!r}")
|
|
162
|
+
return order
|
|
163
|
+
if key == "lang_version":
|
|
164
|
+
text = str(value).strip().lower()
|
|
165
|
+
if text in ("auto", "latest", "preview", "default", "latestmajor"):
|
|
166
|
+
return "auto" if text == "auto" else "latest"
|
|
167
|
+
try:
|
|
168
|
+
float(text)
|
|
169
|
+
except ValueError:
|
|
170
|
+
raise SettingsError(f"{where} must be 'auto', 'latest' or a version such as 12") from None
|
|
171
|
+
return text
|
|
172
|
+
if isinstance(default, bool):
|
|
173
|
+
if isinstance(value, bool):
|
|
174
|
+
return value
|
|
175
|
+
if isinstance(value, str) and value.lower() in ("true", "false"):
|
|
176
|
+
return value.lower() == "true"
|
|
177
|
+
raise SettingsError(f"{where} must be true or false")
|
|
178
|
+
if isinstance(default, int):
|
|
179
|
+
if isinstance(value, bool):
|
|
180
|
+
raise SettingsError(f"{where} must be a whole number")
|
|
181
|
+
try:
|
|
182
|
+
number = int(str(value))
|
|
183
|
+
except ValueError:
|
|
184
|
+
raise SettingsError(f"{where} must be a whole number") from None
|
|
185
|
+
if number < 0 or (key == "indent_size" and number == 0):
|
|
186
|
+
raise SettingsError(f"{where} must be a positive number")
|
|
187
|
+
return number
|
|
188
|
+
text = str(value).strip().lower().replace("-", "_") if key != "charset" else str(value).strip().lower()
|
|
189
|
+
choices = _CHOICES.get(key)
|
|
190
|
+
if choices is not None and text not in choices:
|
|
191
|
+
raise SettingsError(f"{where} must be one of {', '.join(choices)}")
|
|
192
|
+
return text
|
prettycsharp/syntax.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""The syntax model the formatter works on.
|
|
2
|
+
|
|
3
|
+
tree-sitter's tree is copied into ``Node`` objects that can be rearranged by
|
|
4
|
+
the rewrites. Comments are taken out of the tree and attached to tokens as
|
|
5
|
+
trivia: a comment on the same line after a token trails it, and any other
|
|
6
|
+
comment leads the next token. Every token also records how many line breaks
|
|
7
|
+
came before it, which is how blank lines are preserved.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from dataclasses import field as dataclass_field
|
|
15
|
+
|
|
16
|
+
from tree_sitter import Node as TSNode
|
|
17
|
+
from tree_sitter import Tree
|
|
18
|
+
|
|
19
|
+
from prettycsharp.parsing import parse
|
|
20
|
+
|
|
21
|
+
# Node types whose text is printed exactly as written. Interpolated strings
|
|
22
|
+
# carry code and text in one token stream whose whitespace is content.
|
|
23
|
+
ATOMIC_TYPES = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"interpolated_string_expression",
|
|
26
|
+
"string_literal",
|
|
27
|
+
"verbatim_string_literal",
|
|
28
|
+
"raw_string_literal",
|
|
29
|
+
"character_literal",
|
|
30
|
+
"preproc_arg",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
DIRECTIVE_TYPES = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"preproc_if",
|
|
37
|
+
"preproc_elif",
|
|
38
|
+
"preproc_else",
|
|
39
|
+
"preproc_region",
|
|
40
|
+
"preproc_endregion",
|
|
41
|
+
"preproc_define",
|
|
42
|
+
"preproc_undef",
|
|
43
|
+
"preproc_pragma",
|
|
44
|
+
"preproc_nullable",
|
|
45
|
+
"preproc_error",
|
|
46
|
+
"preproc_warning",
|
|
47
|
+
"preproc_line",
|
|
48
|
+
"preproc_if_in_attribute_list",
|
|
49
|
+
"shebang_directive",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Containers whose children are a list of statements or members, where a
|
|
54
|
+
# preprocessor directive can stand between items.
|
|
55
|
+
LIST_TYPES = frozenset(
|
|
56
|
+
{
|
|
57
|
+
"compilation_unit",
|
|
58
|
+
"declaration_list",
|
|
59
|
+
"block",
|
|
60
|
+
"switch_section",
|
|
61
|
+
"switch_body",
|
|
62
|
+
"file_scoped_namespace_declaration",
|
|
63
|
+
"enum_member_declaration_list",
|
|
64
|
+
"preproc_if",
|
|
65
|
+
"preproc_elif",
|
|
66
|
+
"preproc_else",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(eq=False, slots=True)
|
|
72
|
+
class Comment:
|
|
73
|
+
text: str
|
|
74
|
+
start: int
|
|
75
|
+
end: int
|
|
76
|
+
# Line breaks between the previous token or comment and this comment.
|
|
77
|
+
newlines_before: int = 0
|
|
78
|
+
# Whether only whitespace precedes the comment on its line.
|
|
79
|
+
own_line: bool = False
|
|
80
|
+
# Whether a line break follows the comment before the next token.
|
|
81
|
+
newline_after: bool = False
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def is_line(self) -> bool:
|
|
85
|
+
return self.text.startswith("//")
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_doc(self) -> bool:
|
|
89
|
+
return self.text.startswith("///") and not self.text.startswith("////")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(eq=False, slots=True)
|
|
93
|
+
class Node:
|
|
94
|
+
type: str
|
|
95
|
+
named: bool = True
|
|
96
|
+
children: list[Node] = dataclass_field(default_factory=list)
|
|
97
|
+
field: str | None = None
|
|
98
|
+
start: int = -1
|
|
99
|
+
end: int = -1
|
|
100
|
+
# Leaves only: the token's exact text.
|
|
101
|
+
text: str | None = None
|
|
102
|
+
parent: Node | None = None
|
|
103
|
+
leading: list[Comment] = dataclass_field(default_factory=list)
|
|
104
|
+
trailing: list[Comment] = dataclass_field(default_factory=list)
|
|
105
|
+
newlines_before: int = 0
|
|
106
|
+
# Line, column of the start in the original source; -1 when synthesized.
|
|
107
|
+
line: int = -1
|
|
108
|
+
column: int = -1
|
|
109
|
+
end_line: int = -1
|
|
110
|
+
# Printed exactly as written: set for atomic nodes, ignored regions and
|
|
111
|
+
# statements holding a preprocessor directive the printer cannot place.
|
|
112
|
+
verbatim: bool = False
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_token(self) -> bool:
|
|
116
|
+
return self.text is not None
|
|
117
|
+
|
|
118
|
+
def child(self, field_name: str) -> Node | None:
|
|
119
|
+
for c in self.children:
|
|
120
|
+
if c.field == field_name:
|
|
121
|
+
return c
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def children_of(self, field_name: str) -> list[Node]:
|
|
125
|
+
return [c for c in self.children if c.field == field_name]
|
|
126
|
+
|
|
127
|
+
def first_of(self, *types: str) -> Node | None:
|
|
128
|
+
for c in self.children:
|
|
129
|
+
if c.type in types:
|
|
130
|
+
return c
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def named_children(self) -> list[Node]:
|
|
134
|
+
return [c for c in self.children if c.named]
|
|
135
|
+
|
|
136
|
+
def tokens(self) -> Iterator[Node]:
|
|
137
|
+
stack = [self]
|
|
138
|
+
while stack:
|
|
139
|
+
node = stack.pop()
|
|
140
|
+
if node.text is not None:
|
|
141
|
+
yield node
|
|
142
|
+
else:
|
|
143
|
+
stack.extend(reversed(node.children))
|
|
144
|
+
|
|
145
|
+
def first_token(self) -> Node | None:
|
|
146
|
+
node = self
|
|
147
|
+
while node.text is None:
|
|
148
|
+
if not node.children:
|
|
149
|
+
return None
|
|
150
|
+
node = node.children[0]
|
|
151
|
+
return node
|
|
152
|
+
|
|
153
|
+
def last_token(self) -> Node | None:
|
|
154
|
+
node = self
|
|
155
|
+
while node.text is None:
|
|
156
|
+
if not node.children:
|
|
157
|
+
return None
|
|
158
|
+
node = node.children[-1]
|
|
159
|
+
return node
|
|
160
|
+
|
|
161
|
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
162
|
+
if self.text is not None:
|
|
163
|
+
return f"Token({self.type!r}, {self.text!r})"
|
|
164
|
+
return f"Node({self.type!r}, {len(self.children)} children)"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def token(text: str, type: str | None = None) -> Node:
|
|
168
|
+
"""A synthesized token, such as a brace a rewrite adds."""
|
|
169
|
+
return Node(type=type or text, named=False, text=text)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(slots=True)
|
|
173
|
+
class SourceFile:
|
|
174
|
+
source: bytes
|
|
175
|
+
root: Node
|
|
176
|
+
tokens: list[Node]
|
|
177
|
+
# Comments left after the last token, attached to no token.
|
|
178
|
+
trailing_comments: list[Comment]
|
|
179
|
+
eol: str
|
|
180
|
+
|
|
181
|
+
def text(self, start: int, end: int) -> str:
|
|
182
|
+
return self.source[start:end].decode("utf-8")
|
|
183
|
+
|
|
184
|
+
def slice(self, node: Node) -> str:
|
|
185
|
+
return self.text(node.start, node.end)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def detect_eol(text: str) -> str:
|
|
189
|
+
index = text.find("\n")
|
|
190
|
+
if index > 0 and text[index - 1] == "\r":
|
|
191
|
+
return "\r\n"
|
|
192
|
+
if index >= 0:
|
|
193
|
+
return "\n"
|
|
194
|
+
if "\r" in text:
|
|
195
|
+
return "\r"
|
|
196
|
+
return "\n"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build(source: str) -> SourceFile:
|
|
200
|
+
"""Parse ``source`` and build the syntax model, or raise CSharpSyntaxError."""
|
|
201
|
+
tree = parse(source)
|
|
202
|
+
data = source.encode("utf-8")
|
|
203
|
+
root, tokens, comments = _copy(tree, data)
|
|
204
|
+
trailing = _attach(data, tokens, comments)
|
|
205
|
+
_mark_verbatim(root, data, tokens)
|
|
206
|
+
return SourceFile(data, root, tokens, trailing, detect_eol(source))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _copy(tree: Tree, data: bytes) -> tuple[Node, list[Node], list[Comment]]:
|
|
210
|
+
tokens: list[Node] = []
|
|
211
|
+
comments: list[Comment] = []
|
|
212
|
+
root = _new_node(tree.root_node, None, data)
|
|
213
|
+
# (tree-sitter node, our node) pairs whose children are still to copy.
|
|
214
|
+
stack: list[tuple[TSNode, Node]] = [(tree.root_node, root)]
|
|
215
|
+
while stack:
|
|
216
|
+
ts_node, node = stack.pop()
|
|
217
|
+
if node.text is not None:
|
|
218
|
+
continue
|
|
219
|
+
pending: list[tuple[TSNode, Node]] = []
|
|
220
|
+
for i, ts_child in enumerate(ts_node.children):
|
|
221
|
+
if ts_child.type == "comment":
|
|
222
|
+
comments.append(_comment(ts_child, data))
|
|
223
|
+
continue
|
|
224
|
+
child = _new_node(ts_child, ts_node.field_name_for_child(i), data)
|
|
225
|
+
child.parent = node
|
|
226
|
+
node.children.append(child)
|
|
227
|
+
pending.append((ts_child, child))
|
|
228
|
+
stack.extend(reversed(pending))
|
|
229
|
+
tokens.extend(root.tokens())
|
|
230
|
+
comments.sort(key=lambda c: c.start)
|
|
231
|
+
return root, tokens, comments
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _new_node(ts_node: TSNode, field_name: str | None, data: bytes) -> Node:
|
|
235
|
+
text = None
|
|
236
|
+
end = ts_node.end_byte
|
|
237
|
+
if ts_node.child_count == 0 or ts_node.type in ATOMIC_TYPES:
|
|
238
|
+
text = data[ts_node.start_byte : end].decode("utf-8")
|
|
239
|
+
if ts_node.type == "preproc_arg":
|
|
240
|
+
# A directive's argument runs to the line break, and on a CRLF
|
|
241
|
+
# line it takes the \r with it.
|
|
242
|
+
text = text.rstrip()
|
|
243
|
+
end = ts_node.start_byte + len(text.encode("utf-8"))
|
|
244
|
+
return Node(
|
|
245
|
+
type=ts_node.type,
|
|
246
|
+
named=ts_node.is_named,
|
|
247
|
+
field=field_name,
|
|
248
|
+
start=ts_node.start_byte,
|
|
249
|
+
end=end,
|
|
250
|
+
text=text,
|
|
251
|
+
line=ts_node.start_point[0],
|
|
252
|
+
column=ts_node.start_point[1],
|
|
253
|
+
end_line=ts_node.end_point[0],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _comment(ts_node: TSNode, data: bytes) -> Comment:
|
|
258
|
+
text = data[ts_node.start_byte : ts_node.end_byte].decode("utf-8")
|
|
259
|
+
if text.startswith("//"):
|
|
260
|
+
text = text.rstrip("\r\n")
|
|
261
|
+
return Comment(text=text, start=ts_node.start_byte, end=ts_node.start_byte + len(text.encode("utf-8")))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _attach(data: bytes, tokens: list[Node], comments: list[Comment]) -> list[Comment]:
|
|
265
|
+
"""Attach each comment to a token; return those after the last token."""
|
|
266
|
+
trailing_file: list[Comment] = []
|
|
267
|
+
ti = 0
|
|
268
|
+
previous_end = 0
|
|
269
|
+
previous_token: Node | None = None
|
|
270
|
+
ci = 0
|
|
271
|
+
# Walk tokens and comments together in source order.
|
|
272
|
+
while ti < len(tokens) or ci < len(comments):
|
|
273
|
+
next_is_comment = ci < len(comments) and (ti >= len(tokens) or comments[ci].start < tokens[ti].start)
|
|
274
|
+
if next_is_comment:
|
|
275
|
+
comment = comments[ci]
|
|
276
|
+
ci += 1
|
|
277
|
+
gap = data[previous_end : comment.start]
|
|
278
|
+
comment.newlines_before = _count_newlines(gap)
|
|
279
|
+
comment.own_line = comment.newlines_before > 0 or previous_end == 0
|
|
280
|
+
following_end = tokens[ti].start if ti < len(tokens) else len(data)
|
|
281
|
+
following = comments[ci].start if ci < len(comments) else following_end
|
|
282
|
+
comment.newline_after = _count_newlines(data[comment.end : min(following, following_end)]) > 0
|
|
283
|
+
if previous_token is not None and comment.newlines_before == 0 and not _has_leading_pending(previous_token):
|
|
284
|
+
previous_token.trailing.append(comment)
|
|
285
|
+
elif ti < len(tokens):
|
|
286
|
+
tokens[ti].leading.append(comment)
|
|
287
|
+
else:
|
|
288
|
+
trailing_file.append(comment)
|
|
289
|
+
previous_end = comment.end
|
|
290
|
+
else:
|
|
291
|
+
tok = tokens[ti]
|
|
292
|
+
ti += 1
|
|
293
|
+
tok.newlines_before = _count_newlines(data[previous_end : tok.start])
|
|
294
|
+
previous_end = tok.end
|
|
295
|
+
previous_token = tok
|
|
296
|
+
return trailing_file
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _has_leading_pending(token: Node) -> bool:
|
|
300
|
+
# A comment trails a token only if no line break separates them; once a
|
|
301
|
+
# comment on that line was attached as leading to a later token, stop.
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _count_newlines(gap: bytes) -> int:
|
|
306
|
+
# \r\n counts once; a lone \r counts as a line break too.
|
|
307
|
+
return gap.count(b"\n") + gap.count(b"\r") - gap.count(b"\r\n")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _mark_verbatim(root: Node, data: bytes, tokens: list[Node]) -> None:
|
|
311
|
+
# A node whose tokens leave non-whitespace text uncovered between them
|
|
312
|
+
# (tree-sitter keeps some text in no leaf) is printed as written.
|
|
313
|
+
for left, right in zip(tokens, tokens[1:], strict=False):
|
|
314
|
+
gap = data[left.end : right.start]
|
|
315
|
+
if gap.strip() and not _gap_is_comments(gap, left, right):
|
|
316
|
+
ancestor = _common_ancestor(left, right)
|
|
317
|
+
if ancestor is not None:
|
|
318
|
+
ancestor.verbatim = True
|
|
319
|
+
# Directive lines print as written, so no rewrite may touch them: a
|
|
320
|
+
# simple directive, and the condition of #if and #elif.
|
|
321
|
+
for node in _walk(root):
|
|
322
|
+
if node.type in ("preproc_if", "preproc_elif"):
|
|
323
|
+
condition = node.child("condition")
|
|
324
|
+
if condition is not None:
|
|
325
|
+
condition.verbatim = True
|
|
326
|
+
elif node.type in DIRECTIVE_TYPES and node.type != "preproc_else":
|
|
327
|
+
node.verbatim = True
|
|
328
|
+
# A preprocessor directive outside a list makes its list item verbatim.
|
|
329
|
+
for node in _walk(root):
|
|
330
|
+
if node.type in DIRECTIVE_TYPES and node.parent is not None and node.parent.type not in LIST_TYPES:
|
|
331
|
+
item = node.parent
|
|
332
|
+
while item.parent is not None and item.parent.type not in LIST_TYPES:
|
|
333
|
+
item = item.parent
|
|
334
|
+
item.verbatim = True
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _gap_is_comments(gap: bytes, left: Node, right: Node) -> bool:
|
|
338
|
+
text = gap.decode("utf-8")
|
|
339
|
+
for comment in left.trailing + right.leading:
|
|
340
|
+
text = text.replace(comment.text, "", 1)
|
|
341
|
+
return not text.strip()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _common_ancestor(a: Node, b: Node) -> Node | None:
|
|
345
|
+
ancestors = set()
|
|
346
|
+
node: Node | None = a
|
|
347
|
+
while node is not None:
|
|
348
|
+
ancestors.add(id(node))
|
|
349
|
+
node = node.parent
|
|
350
|
+
node = b
|
|
351
|
+
while node is not None:
|
|
352
|
+
if id(node) in ancestors:
|
|
353
|
+
return node
|
|
354
|
+
node = node.parent
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _walk(root: Node) -> Iterator[Node]:
|
|
359
|
+
stack = [root]
|
|
360
|
+
while stack:
|
|
361
|
+
node = stack.pop()
|
|
362
|
+
yield node
|
|
363
|
+
stack.extend(reversed(node.children))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def walk(root: Node) -> Iterator[Node]:
|
|
367
|
+
return _walk(root)
|