Skip to content

Commit edfbf92

Browse files
olehermanseclaude
andcommitted
cfengine format: Fixed formatting of stakeholders and curly brace lists
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ole Herman Schumacher Elgesem <ole.elgesem@northern.tech>
1 parent e4d9b7e commit edfbf92

1 file changed

Lines changed: 158 additions & 25 deletions

File tree

src/cfengine_cli/format.py

Lines changed: 158 additions & 25 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":
@@ -217,18 +325,21 @@ def can_single_line_promise(node, indent, line_length):
217325
has_continuation = next_sib and next_sib.type == "half_promise"
218326
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+
):
335+
return False
336+
prefix = promiser_line(children)
337+
if not prefix:
222338
return False
223339
if attr_children:
224-
line = (
225-
text(promiser_node)
226-
+ " "
227-
+ stringify_single_line_node(attr_children[0])
228-
+ ";"
229-
)
340+
line = prefix + " " + stringify_single_line_node(attr_children[0]) + ";"
230341
else:
231-
line = text(promiser_node) + ";"
342+
line = prefix + ";"
232343
return indent + len(line) <= line_length
233344

234345

@@ -293,23 +404,45 @@ def autoformat(node, fmt, line_length, macro_indent, indent=0):
293404
fmt.print_lines(lines, indent=0)
294405
return
295406
if node.type == "promise":
296-
# Single-line promise: if exactly 1 attribute, no half_promise continuation,
297-
# 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
298418
attr_children = [c for c in children if c.type == "attribute"]
299-
next_sib = node.next_named_sibling
300-
has_continuation = next_sib and next_sib.type == "half_promise"
301-
if len(attr_children) == 1 and not has_continuation:
302-
promiser_node = next((c for c in children if c.type == "promiser"), None)
303-
if promiser_node:
304-
line = (
305-
text(promiser_node)
306-
+ " "
307-
+ stringify_single_line_node(attr_children[0])
308-
+ ";"
309-
)
310-
if indent + len(line) <= line_length:
311-
fmt.print(line, indent)
312-
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
313446
if children:
314447
for child in children:
315448
# Blank line between bundle sections

0 commit comments

Comments
 (0)