Skip to content

Commit ca84699

Browse files
authored
Merge pull request #43 from olehermanse/nick
cfengine format: Inserted empty lines before class guards and promise guards, and removed empty comments
2 parents 6c40b04 + 4c73e8a commit ca84699

File tree

8 files changed

+222
-23
lines changed

8 files changed

+222
-23
lines changed

src/cfengine_cli/format.py

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,18 @@ def update_previous(self, node):
4949
return tmp
5050

5151

52-
def stringify_children_from_strings(parts):
52+
def stringify_parameter_list(parts):
53+
"""Join pre-extracted string tokens into a formatted parameter list.
54+
55+
Used when formatting bundle/body headers. Comments are
56+
stripped from the parameter_list node before this function is called,
57+
so `parts` contains only the structural tokens: "(", identifiers, ","
58+
separators, and ")". The function removes any trailing comma before
59+
")", then joins the tokens with appropriate spacing (space after each
60+
comma, no space after "(" or before ")").
61+
62+
Example: ["(", "a", ",", "b", ",", ")"] -> "(a, b)"
63+
"""
5364
# Remove trailing comma before closing paren
5465
cleaned = []
5566
for i, part in enumerate(parts):
@@ -68,34 +79,47 @@ def stringify_children_from_strings(parts):
6879
return result
6980

7081

71-
def stringify_children(children):
82+
def stringify_single_line_nodes(nodes):
83+
"""Join a list of tree-sitter nodes into a single-line string.
84+
85+
Operates on the direct child nodes of a CFEngine syntax construct
86+
(e.g. a list, call, or attribute). Each child is recursively
87+
flattened via stringify_single_line_node(). Spacing rules:
88+
- A space is inserted after each "," separator.
89+
- A space is inserted before and after "=>" (fat arrow).
90+
- No extra space otherwise (e.g. no space after "(" or before ")").
91+
92+
Used by stringify_single_line_node() to recursively flatten any node with
93+
children, and by maybe_split_generic_list() to attempt a single-line
94+
rendering before falling back to multi-line splitting.
95+
"""
7296
result = ""
7397
previous = None
74-
for child in children:
75-
string = stringify_single_line(child)
98+
for node in nodes:
99+
string = stringify_single_line_node(node)
76100
if previous and previous.type == ",":
77101
result += " "
78-
if previous and child.type == "=>":
102+
if previous and node.type == "=>":
79103
result += " "
80104
if previous and previous.type == "=>":
81105
result += " "
82106
result += string
83-
previous = child
107+
previous = node
84108
return result
85109

86110

87-
def stringify_single_line(node):
111+
def stringify_single_line_node(node):
88112
if not node.children:
89113
return text(node)
90-
return stringify_children(node.children)
114+
return stringify_single_line_nodes(node.children)
91115

92116

93117
def split_generic_value(node, indent, line_length):
94118
if node.type == "call":
95119
return split_rval_call(node, indent, line_length)
96120
if node.type == "list":
97121
return split_rval_list(node, indent, line_length)
98-
return [stringify_single_line(node)]
122+
return [stringify_single_line_node(node)]
99123

100124

101125
def split_generic_list(middle, indent, line_length):
@@ -104,7 +128,7 @@ def split_generic_list(middle, indent, line_length):
104128
if elements and element.type == ",":
105129
elements[-1] = elements[-1] + ","
106130
continue
107-
line = " " * indent + stringify_single_line(element)
131+
line = " " * indent + stringify_single_line_node(element)
108132
if len(line) < line_length:
109133
elements.append(line)
110134
else:
@@ -115,7 +139,7 @@ def split_generic_list(middle, indent, line_length):
115139

116140

117141
def maybe_split_generic_list(nodes, indent, line_length):
118-
string = " " * indent + stringify_children(nodes)
142+
string = " " * indent + stringify_single_line_nodes(nodes)
119143
if len(string) < line_length:
120144
return [string]
121145
return split_generic_list(nodes, indent, line_length)
@@ -147,11 +171,11 @@ def split_rval(node, indent, line_length):
147171
return split_rval_list(node, indent, line_length)
148172
if node.type == "call":
149173
return split_rval_call(node, indent, line_length)
150-
return [stringify_single_line(node)]
174+
return [stringify_single_line_node(node)]
151175

152176

153177
def maybe_split_rval(node, indent, offset, line_length):
154-
line = stringify_single_line(node)
178+
line = stringify_single_line_node(node)
155179
if len(line) + offset < line_length:
156180
return [line]
157181
return split_rval(node, indent, line_length)
@@ -169,11 +193,11 @@ def attempt_split_attribute(node, indent, line_length):
169193
lines = maybe_split_rval(rval, indent, offset, line_length)
170194
lines[0] = prefix + lines[0]
171195
return lines
172-
return [" " * indent + stringify_single_line(node)]
196+
return [" " * indent + stringify_single_line_node(node)]
173197

174198

175199
def stringify(node, indent, line_length):
176-
single_line = " " * indent + stringify_single_line(node)
200+
single_line = " " * indent + stringify_single_line_node(node)
177201
# Reserve 1 char for trailing ; or , after attributes
178202
effective_length = line_length - 1 if node.type == "attribute" else line_length
179203
if len(single_line) < effective_length:
@@ -209,9 +233,7 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
209233
else:
210234
parts.append(text(p))
211235
# Append directly to previous part (no space before parens)
212-
header_parts[-1] = header_parts[-1] + stringify_children_from_strings(
213-
parts
214-
)
236+
header_parts[-1] = header_parts[-1] + stringify_parameter_list(parts)
215237
else:
216238
header_parts.append(text(x))
217239
line = " ".join(header_parts)
@@ -220,7 +242,15 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
220242
if not (prev_sib and prev_sib.type == "comment"):
221243
fmt.print("", 0)
222244
fmt.print(line, 0)
223-
for comment in header_comments:
245+
for i, comment in enumerate(header_comments):
246+
if comment.strip() == "#":
247+
prev_is_comment = i > 0 and header_comments[i - 1].strip() != "#"
248+
next_is_comment = (
249+
i + 1 < len(header_comments)
250+
and header_comments[i + 1].strip() != "#"
251+
)
252+
if not (prev_is_comment and next_is_comment):
253+
continue
224254
fmt.print(comment, 0)
225255
children = node.children[-1].children
226256
if node.type in [
@@ -239,26 +269,37 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
239269
return
240270
if node.type == "promise":
241271
# Single-line promise: if exactly 1 attribute, no half_promise continuation,
242-
# and the whole line fits in line_length
272+
# not inside a class guard, and the whole line fits in line_length
243273
attr_children = [c for c in children if c.type == "attribute"]
244274
next_sib = node.next_named_sibling
245275
has_continuation = next_sib and next_sib.type == "half_promise"
246-
if len(attr_children) == 1 and not has_continuation:
276+
parent = node.parent
277+
in_class_guard = parent and parent.type in [
278+
"class_guarded_promises",
279+
"class_guarded_body_attributes",
280+
"class_guarded_promise_block_attributes",
281+
]
282+
if len(attr_children) == 1 and not has_continuation and not in_class_guard:
247283
promiser_node = next((c for c in children if c.type == "promiser"), None)
248284
if promiser_node:
249285
line = (
250286
text(promiser_node)
251287
+ " "
252-
+ stringify_single_line(attr_children[0])
288+
+ stringify_single_line_node(attr_children[0])
253289
+ ";"
254290
)
255291
if indent + len(line) <= line_length:
256292
fmt.print(line, indent)
257293
return
258294
if children:
259295
for child in children:
296+
# Blank line between bundle sections
297+
if child.type == "bundle_section":
298+
prev = child.prev_named_sibling
299+
if prev and prev.type == "bundle_section":
300+
fmt.print("", 0)
260301
# Blank line between promises in a section
261-
if child.type == "promise":
302+
elif child.type == "promise":
262303
prev = child.prev_named_sibling
263304
if prev and prev.type in ["promise", "half_promise"]:
264305
fmt.print("", 0)
@@ -271,6 +312,7 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
271312
if prev and prev.type in [
272313
"promise",
273314
"half_promise",
315+
"class_guarded_promises",
274316
]:
275317
fmt.print("", 0)
276318
elif child.type == "comment":
@@ -288,6 +330,11 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
288330
fmt.print_same_line(node)
289331
return
290332
if node.type == "comment":
333+
if text(node).strip() == "#":
334+
prev = node.prev_named_sibling
335+
nxt = node.next_named_sibling
336+
if not (prev and prev.type == "comment" and nxt and nxt.type == "comment"):
337+
return
291338
comment_indent = indent
292339
next_sib = node.next_named_sibling
293340
while next_sib and next_sib.type == "comment":

tests/format/002_basics.expected.cf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ bundle agent main
2929
if => "bar"
3030
# Comment at atttribute level
3131
string => "some_value";
32+
33+
classes:
34+
# Comment before promise
35+
"a" if => "b";
3236
}

tests/format/002_basics.input.cf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ baz::
2626
if => "bar"
2727
# Comment at atttribute level
2828
string => "some_value";
29+
classes:
30+
# Comment before promise
31+
"a" if => "b";
2932
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
bundle agent main
2+
{
3+
# comment
4+
reports:
5+
"hello";
6+
}
7+
8+
bundle agent b
9+
{
10+
# Some long comment here
11+
#
12+
# With more explanation here
13+
reports:
14+
"hello";
15+
}
16+
17+
bundle agent c
18+
# Some long comment here
19+
#
20+
# With more explanation here
21+
{
22+
reports:
23+
"hello";
24+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
bundle agent main
2+
{
3+
#
4+
# comment
5+
reports:
6+
"hello";
7+
}
8+
9+
10+
bundle agent b
11+
{
12+
# Some long comment here
13+
#
14+
# With more explanation here
15+
#
16+
reports:
17+
"hello";
18+
}
19+
20+
21+
22+
bundle agent c
23+
# Some long comment here
24+
#
25+
# With more explanation here
26+
#
27+
{
28+
reports:
29+
"hello";
30+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
bundle agent main
2+
{
3+
vars:
4+
hpux::
5+
"package_dir"
6+
string => "$(sys.flavour)_$(sys.arch)";
7+
8+
!hpux::
9+
"package_dir"
10+
string => "$(sys.class)_$(sys.arch)";
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
bundle agent main
2+
{
3+
vars:
4+
hpux::
5+
"package_dir"
6+
string => "$(sys.flavour)_$(sys.arch)";
7+
!hpux::
8+
"package_dir"
9+
string => "$(sys.class)_$(sys.arch)";
10+
}

tests/unit/test_format.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from cfengine_cli.format import stringify_parameter_list, stringify_single_line_nodes
2+
3+
4+
class MockNode:
5+
"""Minimal stand-in for a tree-sitter Node used by stringify_single_line_nodes."""
6+
7+
def __init__(self, node_type, node_text=None, children=None):
8+
self.type = node_type
9+
self.text = node_text.encode("utf-8") if node_text is not None else None
10+
self.children = children or []
11+
12+
13+
def _leaf(node_type, node_text=None):
14+
return MockNode(node_type, node_text or node_type)
15+
16+
17+
def test_stringify_parameter_list():
18+
assert stringify_parameter_list([]) == ""
19+
assert stringify_parameter_list(["foo"]) == "foo"
20+
assert stringify_parameter_list(["(", "a", ")"]) == "(a)"
21+
assert stringify_parameter_list(["(", "a", ",", "b", ")"]) == "(a, b)"
22+
assert stringify_parameter_list(["(", "a", ",", ")"]) == "(a)"
23+
assert stringify_parameter_list(["(", "a", ",", "b", ",", ")"]) == "(a, b)"
24+
assert stringify_parameter_list(["a", "b", "c"]) == "a b c"
25+
assert stringify_parameter_list(["a", ",", "b"]) == "a, b"
26+
assert stringify_parameter_list(["(", ")"]) == "()"
27+
parts = ["(", "x", ",", "y", ",", "z", ")"]
28+
assert stringify_parameter_list(parts) == "(x, y, z)"
29+
30+
31+
def test_stringify_single_line_nodes():
32+
assert stringify_single_line_nodes([]) == ""
33+
assert stringify_single_line_nodes([_leaf("identifier", "foo")]) == "foo"
34+
35+
nodes = [_leaf("string", '"a"'), _leaf(","), _leaf("string", '"b"')]
36+
assert stringify_single_line_nodes(nodes) == '"a", "b"'
37+
38+
nodes = [_leaf("identifier", "lval"), _leaf("=>"), _leaf("string", '"rval"')]
39+
assert stringify_single_line_nodes(nodes) == 'lval => "rval"'
40+
41+
nodes = [_leaf("("), _leaf("identifier", "x"), _leaf(")")]
42+
assert stringify_single_line_nodes(nodes) == "(x)"
43+
44+
nodes = [
45+
_leaf("{"),
46+
_leaf("string", '"a"'),
47+
_leaf(","),
48+
_leaf("string", '"b"'),
49+
_leaf("}"),
50+
]
51+
assert stringify_single_line_nodes(nodes) == '{"a", "b"}'
52+
nodes = [
53+
_leaf("identifier", "package_name"),
54+
_leaf("=>"),
55+
_leaf("string", '"nginx"'),
56+
]
57+
58+
assert stringify_single_line_nodes(nodes) == 'package_name => "nginx"'
59+
inner = MockNode(
60+
"call",
61+
children=[
62+
_leaf("calling_identifier", "func"),
63+
_leaf("("),
64+
_leaf("string", '"arg"'),
65+
_leaf(")"),
66+
],
67+
)
68+
69+
nodes = [_leaf("identifier", "x"), _leaf("=>"), inner]
70+
assert stringify_single_line_nodes(nodes) == 'x => func("arg")'

0 commit comments

Comments
 (0)