3333PROMISER_PARTS = {"promiser" , "->" , "stakeholder" }
3434
3535
36+ def _contains_macro (nodes : Node | list [Node ]) -> bool :
37+ """Check if a node (or any node in a list) is a macro or has a child which is a macro.
38+
39+ This is useful when we want to for example always split something across multiple
40+ lines if macros are involved.
41+ """
42+ if isinstance (nodes , list ):
43+ return any (_contains_macro (node ) for node in nodes )
44+ if nodes .type == "macro" :
45+ return True
46+ return _contains_macro (nodes .children )
47+
48+
3649def format_json_file (filename : str , check : bool ) -> int :
3750 """Reformat a JSON file in place using cfbs pretty-printer.
3851
@@ -192,42 +205,62 @@ def split_generic_value(node: Node, indent: int, line_length: int) -> list[str]:
192205 return [stringify_single_line_node (node )]
193206
194207
208+ def _set_trailing_comma (line : str , add : bool ) -> str :
209+ """Add or remove a trailing comma from a formatted line."""
210+ if add and not line .endswith ("," ):
211+ return line + ","
212+ if not add and line .endswith ("," ):
213+ return line [:- 1 ]
214+ return line
215+
216+
195217def split_generic_list (
196218 middle : list [Node ], indent : int , line_length : int , trailing_comma : bool = True
197219) -> list [str ]:
198220 """Split list elements into one-per-line strings, each pre-indented."""
221+ has_macros = _contains_macro (middle )
199222 elements : list [str ] = []
200223 for element in middle :
201224 if elements and element .type == "," :
202225 elements [- 1 ] = elements [- 1 ] + ","
203226 continue
227+ if element .type == "macro" :
228+ elements .append (text (element ))
229+ continue
204230 line = " " * indent + stringify_single_line_node (element )
205231 if len (line ) < line_length :
206232 elements .append (line )
207233 else :
208234 lines = split_generic_value (element , indent , line_length )
209235 elements .append (" " * indent + lines [0 ])
210236 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
237+
238+ # Optionally add trailing commas (based on trailing comma boolean arg):
239+ # When there are no macros, we just want to find the last element
240+ # which is not a comment, and add a trailing comma if needed.
241+ # If there are macros, there might be 2 elements to fix (1 in each branch).
242+ # Note that syntax errors for missing commas between elements
243+ # should be handled elsewhere.
244+ if has_macros :
245+ for i , e in enumerate (elements ):
246+ if not e .lstrip ().startswith (("@" , "#" )):
247+ elements [i ] = _set_trailing_comma (e , trailing_comma )
248+ else :
249+ for i in reversed (range (len (elements ))):
250+ if not elements [i ].lstrip ().startswith ("#" ):
251+ elements [i ] = _set_trailing_comma (elements [i ], trailing_comma )
252+ break
221253 return elements
222254
223255
224256def maybe_split_generic_list (
225257 nodes : list [Node ], indent : int , line_length : int , trailing_comma : bool = True
226258) -> list [str ]:
227259 """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 ]
260+ if not any (n .type == "macro" for n in nodes ):
261+ string = " " * indent + stringify_single_line_nodes (nodes )
262+ if len (string ) < line_length :
263+ return [string ]
231264 return split_generic_list (nodes , indent , line_length , trailing_comma )
232265
233266
@@ -269,6 +302,8 @@ def maybe_split_rval(
269302 node : Node , indent : int , offset : int , line_length : int
270303) -> list [str ]:
271304 """Return single-line rval if it fits at offset, otherwise split it."""
305+ if _contains_macro (node ):
306+ return split_rval (node , indent , line_length )
272307 line = stringify_single_line_node (node )
273308 if len (line ) + offset < line_length :
274309 return [line ]
@@ -280,8 +315,36 @@ def maybe_split_rval(
280315# ---------------------------------------------------------------------------
281316
282317
318+ def _format_attribute_with_macros (
319+ node : Node , indent : int , line_length : int
320+ ) -> list [str ]:
321+ """Format an attribute whose direct children include macro nodes."""
322+ lines : list [str ] = []
323+ children = list (node .children )
324+
325+ # First two children are lval and arrow (=>)
326+ lval = children [0 ]
327+ arrow = children [1 ]
328+ lines .append (" " * indent + text (lval ) + " " + text (arrow ))
329+
330+ for child in children [2 :]:
331+ if child .type == "macro" :
332+ lines .append (text (child ))
333+ elif child .type == "comment" :
334+ lines .append (" " * (indent + 2 ) + text (child ))
335+ else :
336+ lines .append (" " * (indent + 2 ) + stringify_single_line_node (child ))
337+
338+ return lines
339+
340+
283341def _attempt_split_attribute (node : Node , indent : int , line_length : int ) -> list [str ]:
284342 """Split an attribute node, wrapping the rval if it's a list or call."""
343+ # When macros appear as direct children of the attribute, use
344+ # the dedicated macro-aware formatter.
345+ if any (child .type == "macro" for child in node .children ):
346+ return _format_attribute_with_macros (node , indent , line_length )
347+
285348 assert len (node .children ) >= 3 # lval + arrow + rval + optionally comments
286349
287350 # Separate comments from the 3 structural children (lval, arrow, rval).
@@ -313,6 +376,10 @@ def _attempt_split_attribute(node: Node, indent: int, line_length: int) -> list[
313376
314377def _stringify (node : Node , indent : int , line_length : int ) -> list [str ]:
315378 """Return a node as pre-indented line(s), splitting if it exceeds line_length."""
379+ # Attributes containing macros must always be split — macros cannot
380+ # appear inline on a single line.
381+ if node .type == "attribute" and _contains_macro (node ):
382+ return _attempt_split_attribute (node , indent , line_length - 1 )
316383 single_line = " " * indent + stringify_single_line_node (node )
317384 # Reserve 1 char for trailing ; or , after attributes
318385 effective_length = line_length - 1 if node .type == "attribute" else line_length
@@ -455,6 +522,8 @@ def _can_single_line_promise(node: Node, indent: int, line_length: int) -> bool:
455522 """
456523 assert node .type == "promise"
457524 children = node .children
525+ if _contains_macro (children ):
526+ return False
458527 attrs = [c for c in children if c .type == "attribute" ]
459528 next_sib = node .next_named_sibling
460529 while next_sib and next_sib .type == "macro" :
@@ -762,7 +831,10 @@ def _autoformat(
762831
763832 # Leaf nodes
764833 if node .type in {"," , ";" }:
765- fmt .print_same_line (node )
834+ if previous and previous .type == "macro" :
835+ fmt .print (node , indent + 2 )
836+ else :
837+ fmt .print_same_line (node )
766838 elif node .type == "comment" :
767839 if not _is_empty_comment (node ):
768840 fmt .print (node , _comment_indent (node , indent ))
0 commit comments