Skip to content

Commit 81c2e99

Browse files
olehermanseclaude
andcommitted
cfengine format: Fixed implementation to match test expectations
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Ole Herman Schumacher Elgesem <ole@northern.tech>
1 parent 6c7d14a commit 81c2e99

1 file changed

Lines changed: 66 additions & 7 deletions

File tree

src/cfengine_cli/format.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ def _can_single_line_promise(node: Node, indent: int, line_length: int) -> bool:
536536
return False
537537
if _contains_list_with_comment(children):
538538
return False
539+
if any(c.type == "comment" for c in children):
540+
# Comments between promiser and attribute force multi-line layout
541+
return False
539542
attrs = [c for c in children if c.type == "attribute"]
540543
next_sib = node.next_named_sibling
541544
while next_sib and next_sib.type == "macro":
@@ -647,9 +650,21 @@ def _format_remaining_children(
647650
line_length: int,
648651
) -> None:
649652
"""Format promise children, skipping promiser/arrow/stakeholder parts."""
653+
# When the promise was multi-lined because of comments between the
654+
# promiser and the attribute, preserve the attribute's original spacing
655+
# rather than running it through the spacing-normalizing stringifier.
656+
# Only applies when the comments and attribute aren't separated by a
657+
# macro — half-promise patterns still use the normal renderer.
658+
verbatim_attr = any(c.type == "comment" for c in children) and not any(
659+
c.type == "macro" for c in children
660+
)
650661
for child in children:
651662
if child.type in PROMISER_PARTS:
652663
continue
664+
if verbatim_attr and child.type == "attribute":
665+
fmt.print(text(child), indent + 2)
666+
fmt.update_previous(child)
667+
continue
653668
_autoformat(child, fmt, line_length, indent)
654669

655670

@@ -712,6 +727,30 @@ def _needs_blank_line_before(child: Node, indent: int, line_length: int) -> bool
712727
if not prev:
713728
return False
714729

730+
# Preserve blank lines from the input source for transitions inside
731+
# body/bundle/promise block bodies. Only applies when entering a new
732+
# commented or class-guarded section — blank lines between bare
733+
# attributes are stripped, and class guards with attached leading
734+
# comments take their blank line before the comment.
735+
parent = child.parent
736+
if parent and parent.type in BLOCK_BODY_TYPES:
737+
if child.type == "comment" and not _is_empty_comment(child):
738+
next_sib = _skip_comments(child.next_named_sibling, "next")
739+
# First comment of a chain leading into a class-guarded group:
740+
# take the input blank line from anywhere along the transition.
741+
if (
742+
prev.type != "comment"
743+
and next_sib is not None
744+
and next_sib.type in CLASS_GUARD_TYPES
745+
and next_sib.start_point[0] - prev.end_point[0] >= 2
746+
):
747+
return True
748+
if child.start_point[0] - prev.end_point[0] >= 2:
749+
return True
750+
elif child.type in CLASS_GUARD_TYPES and prev.type != "comment":
751+
if child.start_point[0] - prev.end_point[0] >= 2:
752+
return True
753+
715754
if child.type == "bundle_section":
716755
return prev.type == "bundle_section"
717756

@@ -740,9 +779,13 @@ def _needs_blank_line_before(child: Node, indent: int, line_length: int) -> bool
740779
return prev.type in {"promise", "half_promise", "class_guarded_promises"}
741780

742781
if child.type == "comment":
782+
if _is_empty_comment(child):
783+
return False
784+
# Top-level comment after a complete block — visually separates them
785+
if prev.type in BLOCK_TYPES:
786+
return True
743787
if prev.type not in {"promise", "half_promise"} | CLASS_GUARD_TYPES:
744788
return False
745-
parent = child.parent
746789
if parent and parent.type in {"bundle_section", "class_guarded_promises"}:
747790
return True
748791
# Inside a body/promise block, a comment between two class guards
@@ -790,13 +833,14 @@ def _skip_comments(sibling: Node | None, direction: str = "next") -> Node | None
790833
def _comment_indent(node: Node, indent: int) -> int:
791834
"""Compute indentation for a leaf comment based on its nearest non-comment neighbor."""
792835
nearest = _skip_comments(node.next_named_sibling, "next")
793-
# A trailing comment whose previous sibling is a class-guarded group
794-
# lines up with that group's contents (one extra indent level), as if
795-
# it were the last comment inside the class guard.
836+
# A trailing comment with no next sibling aligns with the deepest
837+
# content at the end of the previous sibling subtree, so that comments
838+
# appended after a class-guarded block visually belong to that block.
796839
if nearest is None:
797-
nearest = _skip_comments(node.prev_named_sibling, "prev")
798-
if nearest and nearest.type in CLASS_GUARD_TYPES:
799-
return indent + 4
840+
prev = _skip_comments(node.prev_named_sibling, "prev")
841+
if prev and prev.type in INDENTED_TYPES:
842+
return _trailing_comment_indent(prev, indent)
843+
nearest = prev
800844
if nearest and nearest.type in INDENTED_TYPES:
801845
return indent + 2
802846
# No indented sibling found — if we're directly inside a block body,
@@ -806,6 +850,21 @@ def _comment_indent(node: Node, indent: int) -> int:
806850
return indent
807851

808852

853+
def _trailing_comment_indent(prev: Node, indent: int) -> int:
854+
"""Walk down the end of a sibling subtree to compute the deepest indent."""
855+
while prev.type in INDENTED_TYPES:
856+
indent += 2
857+
deeper = None
858+
for child in reversed(prev.children):
859+
if child.type in INDENTED_TYPES:
860+
deeper = child
861+
break
862+
if deeper is None:
863+
break
864+
prev = deeper
865+
return indent
866+
867+
809868
# ---------------------------------------------------------------------------
810869
# Main recursive formatter
811870
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)