Skip to content

Commit d68a0dd

Browse files
committed
format: convert single-quoted string literals to double quotes
1 parent 6e273e7 commit d68a0dd

4 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/cfengine_cli/format.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,23 @@ def text(node: Node) -> str:
9595
return node.text.decode("utf-8")
9696

9797

98+
def _normalize_quotes(literal: str) -> str:
99+
"""Normalize a CFEngine string literal to the preferred quote style.
100+
101+
Double quotes are the default. A single-quoted literal is rewritten with
102+
double quotes unless its content contains a double-quote character, in
103+
which case it is left single-quoted to avoid escaping. An escaped single
104+
quote (\\') becomes a plain single quote when converting, since it needs
105+
no escaping inside a double-quoted string.
106+
"""
107+
if len(literal) < 2 or literal[0] != "'" or literal[-1] != "'":
108+
return literal
109+
inner = literal[1:-1]
110+
if '"' in inner:
111+
return literal
112+
return '"' + inner.replace("\\'", "'") + '"'
113+
114+
98115
class Formatter:
99116
"""Accumulates formatted output line-by-line into a string buffer."""
100117

@@ -202,6 +219,8 @@ def stringify_single_line_nodes(nodes: list[Node]) -> str:
202219

203220
def stringify_single_line_node(node: Node) -> str:
204221
"""Recursively flatten a node and its children into a single-line string."""
222+
if node.type == "quoted_string":
223+
return _normalize_quotes(text(node))
205224
if not node.children:
206225
return text(node)
207226
return stringify_single_line_nodes(node.children)
@@ -462,7 +481,7 @@ def _promiser_text(children: list[Node]) -> str | None:
462481
promiser_node = next((c for c in children if c.type == "promiser"), None)
463482
if not promiser_node:
464483
return None
465-
return text(promiser_node)
484+
return _normalize_quotes(text(promiser_node))
466485

467486

468487
def _promiser_line_with_stakeholder(children: list[Node]) -> str | None:
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This demonstrates normalization of string literal quote styles:
2+
# single-quoted literals become double-quoted (the default) unless
3+
# their content contains a double-quote character.
4+
bundle agent main
5+
{
6+
vars:
7+
"a" string => "hello";
8+
"b" string => "world";
9+
"c" string => 'say "hi"';
10+
"d" string => "it's here";
11+
"e" slist => { "one", "two" };
12+
13+
reports:
14+
"a single-quoted promiser";
15+
}

tests/format/012_quotes.input.cf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This demonstrates normalization of string literal quote styles:
2+
# single-quoted literals become double-quoted (the default) unless
3+
# their content contains a double-quote character.
4+
bundle agent main
5+
{
6+
vars:
7+
"a" string => 'hello';
8+
"b" string => "world";
9+
"c" string => 'say "hi"';
10+
"d" string => 'it\'s here';
11+
"e" slist => { 'one', 'two' };
12+
13+
reports:
14+
'a single-quoted promiser';
15+
}

tests/unit/test_format.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,43 @@ def test_format_line_length_respected():
512512
for line in result.strip().split("\n"):
513513
# Allow slight overshoot for long strings that can't be split
514514
assert len(line) <= 80, f"Line too long: {line!r}"
515+
516+
517+
def test_format_single_to_double_quotes():
518+
code = "bundle agent main\n{\nvars:\n\"a\" string => 'hello';\n}"
519+
result = _format(code)
520+
assert '"a" string => "hello";' in result
521+
assert "'hello'" not in result
522+
523+
524+
def test_format_keep_single_quotes_with_inner_double():
525+
code = "bundle agent main\n{\nvars:\n\"a\" string => 'say \"hi\"';\n}"
526+
result = _format(code)
527+
assert "'say \"hi\"'" in result
528+
529+
530+
def test_format_double_quotes_unchanged():
531+
code = 'bundle agent main\n{\nvars:\n"a" string => "world";\n}'
532+
result = _format(code)
533+
assert '"a" string => "world";' in result
534+
535+
536+
def test_format_escaped_single_quote_to_double():
537+
code = "bundle agent main\n{\nvars:\n\"a\" string => 'it\\'s here';\n}"
538+
result = _format(code)
539+
assert '"a" string => "it\'s here";' in result
540+
541+
542+
def test_format_single_quotes_in_list():
543+
code = "bundle agent main\n{\nvars:\n\"a\" slist => { 'one', 'two' };\n}"
544+
result = _format(code)
545+
assert '"one"' in result
546+
assert '"two"' in result
547+
assert "'one'" not in result
548+
549+
550+
def test_format_single_quoted_promiser_to_double():
551+
code = "bundle agent main\n{\nreports:\n 'hello';\n}"
552+
result = _format(code)
553+
assert '"hello"' in result
554+
assert "'hello'" not in result

0 commit comments

Comments
 (0)