Skip to content

Commit 3a7bea3

Browse files
authored
Merge pull request #54 from olehermanse/formatting
cfengine format: Fixed formatting for stakeholders and consecutive 0-attribute single line promises
2 parents 66f300a + edfbf92 commit 3a7bea3

8 files changed

Lines changed: 368 additions & 27 deletions

src/cfengine_cli/format.py

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ def stringify_single_line_nodes(nodes):
103103
result += " "
104104
if previous and previous.type == "=>":
105105
result += " "
106+
if previous and previous.type == "{":
107+
result += " "
108+
if previous and node.type == "}":
109+
result += " "
106110
result += string
107111
previous = node
108112
return result
@@ -207,6 +211,110 @@ def stringify(node, indent, line_length):
207211
return [single_line]
208212

209213

214+
def has_stakeholder(children):
215+
return any(c.type == "stakeholder" for c in children)
216+
217+
218+
def stakeholder_has_comments(children):
219+
stakeholder = next((c for c in children if c.type == "stakeholder"), None)
220+
if not stakeholder:
221+
return False
222+
for child in stakeholder.children:
223+
if child.type == "list":
224+
return any(c.type == "comment" for c in child.children)
225+
return False
226+
227+
228+
def promiser_prefix(children):
229+
"""Build the promiser text (without stakeholder)."""
230+
promiser_node = next((c for c in children if c.type == "promiser"), None)
231+
if not promiser_node:
232+
return None
233+
return text(promiser_node)
234+
235+
236+
def promiser_line(children):
237+
"""Build the promiser prefix: promiser + optional '-> stakeholder'."""
238+
prefix = promiser_prefix(children)
239+
if not prefix:
240+
return None
241+
arrow = next((c for c in children if c.type == "->"), None)
242+
stakeholder = next((c for c in children if c.type == "stakeholder"), None)
243+
if arrow and stakeholder:
244+
prefix += " " + text(arrow) + " " + stringify_single_line_node(stakeholder)
245+
return prefix
246+
247+
248+
def stakeholder_needs_splitting(children, indent, line_length):
249+
"""Check if the stakeholder list needs to be split across multiple lines."""
250+
if stakeholder_has_comments(children):
251+
return True
252+
prefix = promiser_line(children)
253+
if not prefix:
254+
return False
255+
return indent + len(prefix) > line_length
256+
257+
258+
def split_stakeholder(children, indent, has_attributes, line_length):
259+
"""Split a stakeholder list across multiple lines.
260+
261+
Returns (opening_line, element_lines, closing_str) where:
262+
- opening_line: 'promiser -> {' to print at promise indent
263+
- element_lines: pre-indented element strings
264+
- closing_str: '}' or '};' pre-indented at the appropriate level
265+
"""
266+
prefix = promiser_prefix(children)
267+
assert prefix is not None
268+
opening = prefix + " -> {"
269+
stakeholder = next(c for c in children if c.type == "stakeholder")
270+
list_node = next(c for c in stakeholder.children if c.type == "list")
271+
middle = list_node.children[1:-1] # between { and }
272+
element_indent = indent + 4
273+
has_comments = stakeholder_has_comments(children)
274+
if has_attributes or has_comments:
275+
close_indent = indent + 2
276+
else:
277+
close_indent = indent
278+
elements = format_stakeholder_elements(middle, element_indent, line_length)
279+
return opening, elements, close_indent
280+
281+
282+
def has_trailing_comma(middle):
283+
"""Check if a list's middle nodes end with a trailing comma."""
284+
for node in reversed(middle):
285+
if node.type == ",":
286+
return True
287+
if node.type != "comment":
288+
return False
289+
return False
290+
291+
292+
def format_stakeholder_elements(middle, indent, line_length):
293+
"""Format the middle elements of a stakeholder list."""
294+
has_comments = any(n.type == "comment" for n in middle)
295+
if not has_comments:
296+
if has_trailing_comma(middle):
297+
return split_generic_list(middle, indent, line_length)
298+
return maybe_split_generic_list(middle, indent, line_length)
299+
elements = []
300+
for node in middle:
301+
if node.type == ",":
302+
if elements:
303+
elements[-1] = elements[-1] + ","
304+
continue
305+
if node.type == "comment":
306+
elements.append(" " * indent + text(node))
307+
else:
308+
line = " " * indent + stringify_single_line_node(node)
309+
if len(line) < line_length:
310+
elements.append(line)
311+
else:
312+
lines = split_generic_value(node, indent, line_length)
313+
elements.append(" " * indent + lines[0])
314+
elements.extend(lines[1:])
315+
return elements
316+
317+
210318
def can_single_line_promise(node, indent, line_length):
211319
"""Check if a promise node can be formatted on a single line."""
212320
if node.type != "promise":
@@ -215,14 +323,23 @@ def can_single_line_promise(node, indent, line_length):
215323
attr_children = [c for c in children if c.type == "attribute"]
216324
next_sib = node.next_named_sibling
217325
has_continuation = next_sib and next_sib.type == "half_promise"
218-
if len(attr_children) != 1 or has_continuation:
326+
if len(attr_children) > 1 or has_continuation:
219327
return False
220-
promiser_node = next((c for c in children if c.type == "promiser"), None)
221-
if not promiser_node:
328+
# Promises with stakeholder + attributes are always multi-line
329+
if has_stakeholder(children) and attr_children:
330+
return False
331+
# Stakeholders that need splitting can't be single-lined
332+
if has_stakeholder(children) and stakeholder_needs_splitting(
333+
children, indent, line_length
334+
):
222335
return False
223-
line = (
224-
text(promiser_node) + " " + stringify_single_line_node(attr_children[0]) + ";"
225-
)
336+
prefix = promiser_line(children)
337+
if not prefix:
338+
return False
339+
if attr_children:
340+
line = prefix + " " + stringify_single_line_node(attr_children[0]) + ";"
341+
else:
342+
line = prefix + ";"
226343
return indent + len(line) <= line_length
227344

228345

@@ -287,23 +404,45 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
287404
fmt.print_lines(lines, indent=0)
288405
return
289406
if node.type == "promise":
290-
# Single-line promise: if exactly 1 attribute, no half_promise continuation,
291-
# not inside a class guard, and the whole line fits in line_length
407+
if can_single_line_promise(node, indent, line_length):
408+
prefix = promiser_line(children)
409+
assert prefix is not None
410+
attr_node = next((c for c in children if c.type == "attribute"), None)
411+
if attr_node:
412+
line = prefix + " " + stringify_single_line_node(attr_node) + ";"
413+
else:
414+
line = prefix + ";"
415+
fmt.print(line, indent)
416+
return
417+
# Multi-line promise with stakeholder that needs splitting
292418
attr_children = [c for c in children if c.type == "attribute"]
293-
next_sib = node.next_named_sibling
294-
has_continuation = next_sib and next_sib.type == "half_promise"
295-
if len(attr_children) == 1 and not has_continuation:
296-
promiser_node = next((c for c in children if c.type == "promiser"), None)
297-
if promiser_node:
298-
line = (
299-
text(promiser_node)
300-
+ " "
301-
+ stringify_single_line_node(attr_children[0])
302-
+ ";"
303-
)
304-
if indent + len(line) <= line_length:
305-
fmt.print(line, indent)
306-
return
419+
if has_stakeholder(children) and stakeholder_needs_splitting(
420+
children, indent, line_length
421+
):
422+
opening, elements, close_indent = split_stakeholder(
423+
children, indent, bool(attr_children), line_length
424+
)
425+
fmt.print(opening, indent)
426+
fmt.print_lines(elements, indent=0)
427+
if attr_children:
428+
fmt.print("}", close_indent)
429+
else:
430+
fmt.print("};", close_indent)
431+
return
432+
for child in children:
433+
if child.type in {"promiser", "->", "stakeholder"}:
434+
continue
435+
autoformat(child, fmt, line_length, macro_indent, indent)
436+
return
437+
# Multi-line promise: print promiser (with stakeholder) then recurse for rest
438+
prefix = promiser_line(children)
439+
if prefix:
440+
fmt.print(prefix, indent)
441+
for child in children:
442+
if child.type in {"promiser", "->", "stakeholder"}:
443+
continue
444+
autoformat(child, fmt, line_length, macro_indent, indent)
445+
return
307446
if children:
308447
for child in children:
309448
# Blank line between bundle sections

tests/format/002_basics.expected.cf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# too deep into edge cases and wrappting etc.
33
body common control
44
{
5-
inputs => {"/var/cfengine/inputs/some_file.cf"};
5+
inputs => { "/var/cfengine/inputs/some_file.cf" };
66
linux::
7-
inputs => {"/var/cfengine/inputs/other_file.cf"};
7+
inputs => { "/var/cfengine/inputs/other_file.cf" };
88
}
99

1010
promise agent example

tests/format/003_wrapping.expected.cf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ bundle agent strings
2121
bundle agent slists
2222
{
2323
vars:
24-
"variable_name" slist => {"one", "two", "three", "four", "five", "six"};
24+
"variable_name" slist => { "one", "two", "three", "four", "five", "six" };
2525

2626
"variable_name"
27-
slist => {"one", "two", "three", "four", "five", "six", "seven"};
27+
slist => { "one", "two", "three", "four", "five", "six", "seven" };
2828

2929
"variable_name"
3030
slist => {

tests/format/009_single_line.expected.cf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ bundle agent main
1010
"libssl-dev" package_policy => "delete";
1111
"libpcre2-dev" package_policy => "delete";
1212
"libacl1-dev" package_policy => "delete";
13+
"fail2ban" comment => "Ban IPs with repeated failed SSH auth attempts";
14+
"binutils";
15+
"bison";
16+
"build-essential";
17+
"libltdl7" package_policy => "delete";
18+
"libltdl-dev" package_policy => "delete";
1319
}

tests/format/009_single_line.input.cf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,15 @@ bundle agent main
2323
"libacl1-dev"
2424
package_policy => "delete";
2525

26+
"fail2ban" comment => "Ban IPs with repeated failed SSH auth attempts";
27+
28+
"binutils";
29+
30+
"bison";
31+
32+
"build-essential";
33+
34+
"libltdl7" package_policy => "delete";
35+
36+
"libltdl-dev" package_policy => "delete";
2637
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
bundle agent main
2+
{
3+
packages:
4+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" };
5+
6+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" }
7+
comment => "foo";
8+
9+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" }
10+
comment => "Long comment that probably definitely exceeds the set line length limit";
11+
12+
"platform-python-devel" -> {
13+
# foo
14+
};
15+
16+
"platform-python-devel" -> {
17+
# foo
18+
"bar",
19+
# baz
20+
};
21+
22+
"python3-rpm-macros" -> {
23+
"reasons for the change or where to look or who to blame", "ENT-1234"
24+
}
25+
comment => "foo";
26+
27+
"python3-rpm-macros" -> {
28+
"reasons for the change or where to look or who to blame longer than line limit",
29+
"ENT-1234",
30+
}
31+
comment => "foo";
32+
33+
"python3-rpm-macros" -> {
34+
"many",
35+
"different",
36+
"stakeholders",
37+
"for",
38+
"this",
39+
"promise",
40+
}
41+
comment => "foo";
42+
43+
"python3-rpm-macros" -> {
44+
"many",
45+
"different",
46+
"stakeholders",
47+
"for",
48+
"this",
49+
"promise",
50+
};
51+
52+
packages:
53+
any::
54+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" };
55+
56+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" }
57+
comment => "foo";
58+
59+
"platform-python-devel" -> { "cfbs shebang", "ENT-1234" }
60+
comment => "Long comment that probably definitely exceeds the set line length limit";
61+
62+
"platform-python-devel" -> {
63+
# foo
64+
};
65+
66+
"platform-python-devel" -> {
67+
# foo
68+
"bar",
69+
# baz
70+
};
71+
72+
"python3-rpm-macros" -> {
73+
"reasons for the change or where to look or who to blame", "ENT-1234"
74+
}
75+
comment => "foo";
76+
77+
"python3-rpm-macros" -> {
78+
"reasons for the change or where to look or who to blame longer than line limit",
79+
"ENT-1234",
80+
}
81+
comment => "foo";
82+
83+
"python3-rpm-macros" -> {
84+
"many",
85+
"different",
86+
"stakeholders",
87+
"for",
88+
"this",
89+
"promise",
90+
}
91+
comment => "foo";
92+
93+
"python3-rpm-macros" -> {
94+
"many",
95+
"different",
96+
"stakeholders",
97+
"for",
98+
"this",
99+
"promise",
100+
};
101+
}

0 commit comments

Comments
 (0)