3333PROMISER_PARTS = {"promiser" , "->" , "stakeholder" }
3434
3535
36+ def _has_direct_macro (node : Node ) -> bool :
37+ """Check if any direct child of a node is a macro (non-recursive)."""
38+ return any (child .type == "macro" for child in node .children )
39+
40+
41+ def _contains_macro (nodes : Node | list [Node ]) -> bool :
42+ """Check if a node (or any node in a list) is a macro or has a child which is a macro.
43+
44+ This is useful when we want to for example always split something across multiple
45+ lines if macros are involved.
46+ """
47+ if isinstance (nodes , list ):
48+ return any (_contains_macro (node ) for node in nodes )
49+ if nodes .type == "macro" :
50+ return True
51+ return _contains_macro (nodes .children )
52+
53+
3654def format_json_file (filename : str , check : bool ) -> int :
3755 """Reformat a JSON file in place using cfbs pretty-printer.
3856
@@ -192,42 +210,62 @@ def split_generic_value(node: Node, indent: int, line_length: int) -> list[str]:
192210 return [stringify_single_line_node (node )]
193211
194212
213+ def _set_trailing_comma (line : str , add : bool ) -> str :
214+ """Add or remove a trailing comma from a formatted line."""
215+ if add and not line .endswith ("," ):
216+ return line + ","
217+ if not add and line .endswith ("," ):
218+ return line [:- 1 ]
219+ return line
220+
221+
195222def split_generic_list (
196223 middle : list [Node ], indent : int , line_length : int , trailing_comma : bool = True
197224) -> list [str ]:
198225 """Split list elements into one-per-line strings, each pre-indented."""
226+ has_macros = _contains_macro (middle )
199227 elements : list [str ] = []
200228 for element in middle :
201229 if elements and element .type == "," :
202230 elements [- 1 ] = elements [- 1 ] + ","
203231 continue
232+ if element .type == "macro" :
233+ elements .append (text (element ))
234+ continue
204235 line = " " * indent + stringify_single_line_node (element )
205236 if len (line ) < line_length :
206237 elements .append (line )
207238 else :
208239 lines = split_generic_value (element , indent , line_length )
209240 elements .append (" " * indent + lines [0 ])
210241 elements .extend (lines [1 :])
211- # Ensure trailing comma state matches the desired setting, on the last
212- # non-comment element (so it doesn't end up after a trailing comment).
213- for i in range (len (elements ) - 1 , - 1 , - 1 ):
214- if elements [i ].lstrip ().startswith ("#" ):
215- continue
216- if trailing_comma and not elements [i ].endswith ("," ):
217- elements [i ] = elements [i ] + ","
218- elif not trailing_comma and elements [i ].endswith ("," ):
219- elements [i ] = elements [i ][:- 1 ]
220- break
242+
243+ # Optionally add trailing commas (based on trailing comma boolean arg):
244+ # When there are no macros, we just want to find the last element
245+ # which is not a comment, and add a trailing comma if needed.
246+ # If there are macros, there might be 2 elements to fix (1 in each branch).
247+ # Note that syntax errors for missing commas between elements
248+ # should be handled elsewhere.
249+ if has_macros :
250+ for i , e in enumerate (elements ):
251+ if not e .lstrip ().startswith (("@" , "#" )):
252+ elements [i ] = _set_trailing_comma (e , trailing_comma )
253+ else :
254+ for i in reversed (range (len (elements ))):
255+ if not elements [i ].lstrip ().startswith ("#" ):
256+ elements [i ] = _set_trailing_comma (elements [i ], trailing_comma )
257+ break
221258 return elements
222259
223260
224261def maybe_split_generic_list (
225262 nodes : list [Node ], indent : int , line_length : int , trailing_comma : bool = True
226263) -> list [str ]:
227264 """Try a single-line rendering; fall back to split_generic_list if too long."""
228- string = " " * indent + stringify_single_line_nodes (nodes )
229- if len (string ) < line_length :
230- return [string ]
265+ if not any (n .type == "macro" for n in nodes ):
266+ string = " " * indent + stringify_single_line_nodes (nodes )
267+ if len (string ) < line_length :
268+ return [string ]
231269 return split_generic_list (nodes , indent , line_length , trailing_comma )
232270
233271
@@ -269,6 +307,8 @@ def maybe_split_rval(
269307 node : Node , indent : int , offset : int , line_length : int
270308) -> list [str ]:
271309 """Return single-line rval if it fits at offset, otherwise split it."""
310+ if _contains_macro (node ):
311+ return split_rval (node , indent , line_length )
272312 line = stringify_single_line_node (node )
273313 if len (line ) + offset < line_length :
274314 return [line ]
@@ -280,8 +320,36 @@ def maybe_split_rval(
280320# ---------------------------------------------------------------------------
281321
282322
323+ def _format_attribute_with_macros (
324+ node : Node , indent : int , line_length : int
325+ ) -> list [str ]:
326+ """Format an attribute whose direct children include macro nodes."""
327+ lines : list [str ] = []
328+ children = list (node .children )
329+
330+ # First two children are lval and arrow (=>)
331+ lval = children [0 ]
332+ arrow = children [1 ]
333+ lines .append (" " * indent + text (lval ) + " " + text (arrow ))
334+
335+ for child in children [2 :]:
336+ if child .type == "macro" :
337+ lines .append (text (child ))
338+ elif child .type == "comment" :
339+ lines .append (" " * (indent + 2 ) + text (child ))
340+ else :
341+ lines .append (" " * (indent + 2 ) + stringify_single_line_node (child ))
342+
343+ return lines
344+
345+
283346def _attempt_split_attribute (node : Node , indent : int , line_length : int ) -> list [str ]:
284347 """Split an attribute node, wrapping the rval if it's a list or call."""
348+ # When macros appear as direct children of the attribute, use
349+ # the dedicated macro-aware formatter.
350+ if _has_direct_macro (node ):
351+ return _format_attribute_with_macros (node , indent , line_length )
352+
285353 assert len (node .children ) >= 3 # lval + arrow + rval + optionally comments
286354
287355 # Separate comments from the 3 structural children (lval, arrow, rval).
@@ -313,6 +381,10 @@ def _attempt_split_attribute(node: Node, indent: int, line_length: int) -> list[
313381
314382def _stringify (node : Node , indent : int , line_length : int ) -> list [str ]:
315383 """Return a node as pre-indented line(s), splitting if it exceeds line_length."""
384+ # Attributes containing macros must always be split — macros cannot
385+ # appear inline on a single line.
386+ if node .type == "attribute" and _contains_macro (node ):
387+ return _attempt_split_attribute (node , indent , line_length - 1 )
316388 single_line = " " * indent + stringify_single_line_node (node )
317389 # Reserve 1 char for trailing ; or , after attributes
318390 effective_length = line_length - 1 if node .type == "attribute" else line_length
@@ -455,6 +527,8 @@ def _can_single_line_promise(node: Node, indent: int, line_length: int) -> bool:
455527 """
456528 assert node .type == "promise"
457529 children = node .children
530+ if _contains_macro (children ):
531+ return False
458532 attrs = [c for c in children if c .type == "attribute" ]
459533 next_sib = node .next_named_sibling
460534 while next_sib and next_sib .type == "macro" :
@@ -762,7 +836,10 @@ def _autoformat(
762836
763837 # Leaf nodes
764838 if node .type in {"," , ";" }:
765- fmt .print_same_line (node )
839+ if previous and previous .type == "macro" :
840+ fmt .print (node , indent + 2 )
841+ else :
842+ fmt .print_same_line (node )
766843 elif node .type == "comment" :
767844 if not _is_empty_comment (node ):
768845 fmt .print (node , _comment_indent (node , indent ))
0 commit comments