Skip to content

Commit f18e8d9

Browse files
authored
Merge pull request #71 from olehermanse/macros
cfengine format: Fixed formatting around macros
2 parents bc9a68d + ba0c753 commit f18e8d9

3 files changed

Lines changed: 210 additions & 27 deletions

File tree

src/cfengine_cli/format.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def __init__(self) -> None:
6666
self.empty: bool = True
6767
self.previous: Node | None = None
6868
self.buffer: str = ""
69+
# Indentation level saved when @if is encountered, restored after
70+
# @else so that both branches are formatted at the same depth.
71+
self.macro_indent: int = 0
6972

7073
def _write(self, message: str, end: str = "\n") -> None:
7174
"""Append text to the buffer with the given line ending."""
@@ -432,6 +435,8 @@ def can_single_line_promise(node: Node, indent: int, line_length: int) -> bool:
432435
children = node.children
433436
attrs = [c for c in children if c.type == "attribute"]
434437
next_sib = node.next_named_sibling
438+
while next_sib and next_sib.type == "macro":
439+
next_sib = next_sib.next_named_sibling
435440
if len(attrs) > 1:
436441
# We always want to multiline a promise with multiple attributes
437442
# even if it would fit on one line, i.e this should be split:
@@ -479,7 +484,6 @@ def _format_promise(
479484
fmt: Formatter,
480485
indent: int,
481486
line_length: int,
482-
macro_indent: int,
483487
) -> bool:
484488
"""Format a promise node. Returns True if handled, False to fall through."""
485489
# Single-line promise
@@ -513,7 +517,7 @@ def _format_promise(
513517
close_indent = indent + 2
514518
if attrs:
515519
fmt.print("}", close_indent)
516-
_format_remaining_children(children, fmt, indent, line_length, macro_indent)
520+
_format_remaining_children(children, fmt, indent, line_length)
517521
else:
518522
fmt.print("};", close_indent)
519523
return True
@@ -522,7 +526,7 @@ def _format_promise(
522526
prefix = _promiser_line_with_stakeholder(children)
523527
if prefix:
524528
fmt.print(prefix, indent)
525-
_format_remaining_children(children, fmt, indent, line_length, macro_indent)
529+
_format_remaining_children(children, fmt, indent, line_length)
526530
return True
527531

528532
return False
@@ -533,13 +537,12 @@ def _format_remaining_children(
533537
fmt: Formatter,
534538
indent: int,
535539
line_length: int,
536-
macro_indent: int,
537540
) -> None:
538541
"""Format promise children, skipping promiser/arrow/stakeholder parts."""
539542
for child in children:
540543
if child.type in PROMISER_PARTS:
541544
continue
542-
autoformat(child, fmt, line_length, macro_indent, indent)
545+
autoformat(child, fmt, line_length, indent)
543546

544547

545548
# ---------------------------------------------------------------------------
@@ -570,7 +573,10 @@ def _format_block_header(node: Node, fmt: Formatter) -> list[Node]:
570573
# Skip over preceding empty comments since they will be removed
571574
while prev_sib and prev_sib.type == "comment" and _is_empty_comment(prev_sib):
572575
prev_sib = prev_sib.prev_named_sibling
573-
if not (prev_sib and prev_sib.type == "comment"):
576+
is_macro_wrapped = (
577+
prev_sib and prev_sib.type == "macro" and text(prev_sib).startswith("@if")
578+
)
579+
if not (prev_sib and prev_sib.type == "comment") and not is_macro_wrapped:
574580
fmt.blank_line()
575581
fmt.print(line, 0)
576582
for i, comment in enumerate(header_comments):
@@ -599,14 +605,20 @@ def _needs_blank_line_before(child: Node, indent: int, line_length: int) -> bool
599605
if child.type == "bundle_section":
600606
return prev.type == "bundle_section"
601607

602-
if child.type == "promise" and prev.type in {"promise", "half_promise"}:
603-
promise_indent = indent + 2
604-
both_single = (
605-
prev.type == "promise"
606-
and can_single_line_promise(prev, promise_indent, line_length)
607-
and can_single_line_promise(child, promise_indent, line_length)
608-
)
609-
return not both_single
608+
if child.type == "promise":
609+
# Skip past macros to find the content-bearing previous sibling,
610+
# so we detect promise groups separated by macro-split halves.
611+
prev_content = prev
612+
while prev_content and prev_content.type == "macro":
613+
prev_content = prev_content.prev_named_sibling
614+
if prev_content and prev_content.type in {"promise", "half_promise"}:
615+
promise_indent = indent + 2
616+
both_single = (
617+
prev_content.type == "promise"
618+
and can_single_line_promise(prev_content, promise_indent, line_length)
619+
and can_single_line_promise(child, promise_indent, line_length)
620+
)
621+
return not both_single
610622

611623
if child.type in CLASS_GUARD_TYPES:
612624
return prev.type in {"promise", "half_promise", "class_guarded_promises"}
@@ -637,8 +649,8 @@ def _is_empty_comment(node: Node) -> bool:
637649

638650

639651
def _skip_comments(sibling: Node | None, direction: str = "next") -> Node | None:
640-
"""Walk past adjacent comment siblings to find the nearest non-comment."""
641-
while sibling and sibling.type == "comment":
652+
"""Walk past adjacent comment and macro siblings to find the nearest content node."""
653+
while sibling and sibling.type in ("comment", "macro"):
642654
sibling = (
643655
sibling.next_named_sibling
644656
if direction == "next"
@@ -670,21 +682,25 @@ def autoformat(
670682
node: Node,
671683
fmt: Formatter,
672684
line_length: int,
673-
macro_indent: int,
674685
indent: int = 0,
675686
) -> None:
676687
"""Recursively format a tree-sitter node tree into the Formatter buffer."""
677688
previous = fmt.update_previous(node)
678689

679690
# Macro handling
680691
if previous and previous.type == "macro" and text(previous).startswith("@else"):
681-
indent = macro_indent
692+
if node.type != "half_promise":
693+
indent = fmt.macro_indent
682694
if node.type == "macro":
683-
fmt.print(node, 0)
684695
if text(node).startswith("@if"):
685-
macro_indent = indent
696+
# Add blank line before @if at top level if preceded by a block
697+
prev_sib = node.prev_named_sibling
698+
if prev_sib and prev_sib.type in BLOCK_TYPES and not fmt.empty:
699+
fmt.blank_line()
700+
fmt.macro_indent = indent
686701
elif text(node).startswith("@else"):
687-
indent = macro_indent
702+
indent = fmt.macro_indent
703+
fmt.print(node, 0)
688704
return
689705

690706
# Block header (bundle/body/promise blocks)
@@ -703,15 +719,15 @@ def autoformat(
703719

704720
# Promise — delegate to promise formatter
705721
if node.type == "promise":
706-
if _format_promise(node, children, fmt, indent, line_length, macro_indent):
722+
if _format_promise(node, children, fmt, indent, line_length):
707723
return
708724

709725
# Interior node with children — recurse
710726
if children:
711727
for child in children:
712728
if _needs_blank_line_before(child, indent, line_length):
713729
fmt.blank_line()
714-
autoformat(child, fmt, line_length, macro_indent, indent)
730+
autoformat(child, fmt, line_length, indent)
715731
return
716732

717733
# Leaf nodes
@@ -739,15 +755,14 @@ def format_policy_file(filename: str, line_length: int, check: bool) -> int:
739755
PY_LANGUAGE = Language(tscfengine.language())
740756
parser = Parser(PY_LANGUAGE)
741757

742-
macro_indent = 0
743758
fmt = Formatter()
744759
with open(filename, "rb") as f:
745760
original_data = f.read()
746761
tree = parser.parse(original_data)
747762

748763
root_node = tree.root_node
749764
assert root_node.type == "source_file"
750-
autoformat(root_node, fmt, line_length, macro_indent)
765+
autoformat(root_node, fmt, line_length)
751766

752767
new_data = fmt.buffer + "\n"
753768
if new_data != original_data.decode("utf-8"):
@@ -768,14 +783,13 @@ def format_policy_fin_fout(
768783
PY_LANGUAGE = Language(tscfengine.language())
769784
parser = Parser(PY_LANGUAGE)
770785

771-
macro_indent = 0
772786
fmt = Formatter()
773787
original_data = fin.read().encode("utf-8")
774788
tree = parser.parse(original_data)
775789

776790
root_node = tree.root_node
777791
assert root_node.type == "source_file"
778-
autoformat(root_node, fmt, line_length, macro_indent)
792+
autoformat(root_node, fmt, line_length)
779793

780794
new_data = fmt.buffer + "\n"
781795
fout.write(new_data)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
bundle agent main
2+
{
3+
vars:
4+
"v" string => "hello";
5+
@if minimum_version(3.18)
6+
"new_var" string => "only in 3.18+";
7+
@endif
8+
reports:
9+
"Hello!";
10+
@if minimum_version(3.20)
11+
"New report";
12+
@endif
13+
}
14+
15+
bundle agent other
16+
{
17+
@if minimum_version(3.18)
18+
vars:
19+
"v" string => "value";
20+
@else
21+
vars:
22+
"v" string => "old_value";
23+
@endif
24+
}
25+
26+
@if minimum_version(3.18)
27+
bundle agent new_bundle
28+
{
29+
reports:
30+
"Only available in 3.18+";
31+
}
32+
@endif
33+
34+
bundle agent half_promises
35+
{
36+
vars:
37+
"promiser"
38+
@if minimum_version(3.18)
39+
string => "new_value";
40+
@else
41+
string => "old_value";
42+
@endif
43+
44+
"another"
45+
@if minimum_version(3.18)
46+
string => "new";
47+
@else
48+
string => "old";
49+
@endif
50+
}
51+
52+
bundle agent comments_and_macros
53+
{
54+
vars:
55+
# Comment before macro
56+
@if minimum_version(3.18)
57+
"v" string => "new";
58+
@endif
59+
# Comment after macro
60+
"w" string => "always";
61+
@if minimum_version(3.18)
62+
# Comment inside macro block
63+
"x" string => "new_with_comment";
64+
@endif
65+
reports:
66+
# Report comment
67+
@if minimum_version(3.20)
68+
# Another comment
69+
"New report";
70+
@endif
71+
"Always report";
72+
}
73+
74+
bundle agent half_with_comment
75+
{
76+
vars:
77+
# Comment before half promise
78+
"promiser"
79+
# Comment before macro in half promise
80+
@if minimum_version(3.18)
81+
string => "new_value";
82+
@else
83+
string => "old_value";
84+
@endif
85+
}

tests/format/011_macros.input.cf

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
bundle agent main
2+
{
3+
vars:
4+
"v" string => "hello";
5+
@if minimum_version(3.18)
6+
"new_var" string => "only in 3.18+";
7+
@endif
8+
reports:
9+
"Hello!";
10+
@if minimum_version(3.20)
11+
"New report";
12+
@endif
13+
}
14+
15+
bundle agent other
16+
{
17+
@if minimum_version(3.18)
18+
vars:
19+
"v" string => "value";
20+
@else
21+
vars:
22+
"v" string => "old_value";
23+
@endif
24+
}
25+
26+
@if minimum_version(3.18)
27+
bundle agent new_bundle
28+
{
29+
reports:
30+
"Only available in 3.18+";
31+
}
32+
@endif
33+
34+
bundle agent half_promises
35+
{
36+
vars:
37+
"promiser"
38+
@if minimum_version(3.18)
39+
string => "new_value";
40+
@else
41+
string => "old_value";
42+
@endif
43+
"another"
44+
@if minimum_version(3.18)
45+
string => "new";
46+
@else
47+
string => "old";
48+
@endif
49+
}
50+
51+
bundle agent comments_and_macros
52+
{
53+
vars:
54+
# Comment before macro
55+
@if minimum_version(3.18)
56+
"v" string => "new";
57+
@endif
58+
# Comment after macro
59+
"w" string => "always";
60+
@if minimum_version(3.18)
61+
# Comment inside macro block
62+
"x" string => "new_with_comment";
63+
@endif
64+
reports:
65+
# Report comment
66+
@if minimum_version(3.20)
67+
# Another comment
68+
"New report";
69+
@endif
70+
"Always report";
71+
}
72+
73+
bundle agent half_with_comment
74+
{
75+
vars:
76+
# Comment before half promise
77+
"promiser"
78+
# Comment before macro in half promise
79+
@if minimum_version(3.18)
80+
string => "new_value";
81+
@else
82+
string => "old_value";
83+
@endif
84+
}

0 commit comments

Comments
 (0)