From fee64ccb2dfad79a3a5940fa495e51d5ed6d9acf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se>
Date: Tue, 19 Sep 2023 15:24:25 +0200
Subject: [PATCH] Fix index access in interpolated strings.

---
 muppet/puppet/format/parser.py | 38 ++++++++++++++++
 tests/test_parse_elsif.py      | 79 ++++++++++++++++++++++++++++++++++
 2 files changed, 117 insertions(+)

diff --git a/muppet/puppet/format/parser.py b/muppet/puppet/format/parser.py
index 046823c..b5406a9 100644
--- a/muppet/puppet/format/parser.py
+++ b/muppet/puppet/format/parser.py
@@ -289,6 +289,39 @@ class ParserFormatter(Serializer[ParseDirective]):
             case _:
                 return nop
 
+    def concat_access(self, item: PuppetAccess) -> ParseDirective:
+        """
+        Parse an access inside an interpolated string.
+
+        The following string
+
+        .. code-block:: puppet
+
+            "Hello ${people['name']['first']}!"
+
+        serializes more or less ass
+
+        .. code-block:: lisp
+
+            (concat
+              (str "Hello ")
+              (access (access (var people) 'name') 'first'))
+
+        And the regular PuppetAccess procedure can't be used, since
+        the leading '$' is optional here.
+
+        Note that the delimiting "${" and "}" should be handled
+        outside this procedure.
+        """
+        parser = ws
+        match item:
+            case PuppetAccess(PuppetVar(name), _):
+                parser &= optional(s('$')) & name
+            case PuppetAccess(PuppetAccess() as next, _):
+                parser &= self.concat_access(next)
+        parser &= ws & self.known_array("[]", item.args)
+        return parser
+
     # --------------------------------------------------
 
     @override
@@ -363,6 +396,11 @@ class ParserFormatter(Serializer[ParseDirective]):
                     except ParseError:
                         for c in st:
                             parser &= rich_char(c)
+                case PuppetAccess():
+                    # Needs to be separate from the "regular"
+                    # PuppetAccess rule, since these variables
+                    parser &= ws & "${" & self.concat_access(fragment)
+                    parser &= ws & "}"
                 case _:
                     # TODO "${x[10][20]}"
                     parser &= ws & "${" & ws & self.s(fragment) & ws & "}"
diff --git a/tests/test_parse_elsif.py b/tests/test_parse_elsif.py
index 9230420..88eb69e 100644
--- a/tests/test_parse_elsif.py
+++ b/tests/test_parse_elsif.py
@@ -1,5 +1,6 @@
 """Tests for Parser combinator re-parsers."""
 
+import pytest
 from muppet.puppet.format.parser import ParserFormatter
 from muppet.puppet.ast import build_ast
 from muppet.puppet.parser import puppet_parser
@@ -151,3 +152,81 @@ def test_funcall_with_block_inner():
     print("parser:\n" + str(parser))
     match_objects = ParserCombinator(s, "s").get(parser)
     pprint(match_objects)
+
+
+def test_string_interpolation_1():
+    # This one is parsed as 'qn'
+    s = """
+    "${x}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
+
+
+def test_string_interpolation_2():
+    # This one is parsed as 'var', but also looks like one
+    s = """
+    "${$x}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
+
+
+def test_string_interpolation_3():
+    # qn
+    s = """
+    "${x + 1}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
+
+
+def test_string_interpolation_4():
+    # var
+    s = """
+    "${x + 1}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
+
+
+def test_string_interpolation_access():
+    # var, but looks like qn
+    s = """
+    "${x['y']}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
+
+
+def test_string_interpolation_deep_access():
+    # var, but looks like qn
+    s = """
+    "${x['y']['z']}"
+    """
+    ast = build_ast(puppet_parser(s))
+    pprint(ast)
+    parser = ParserFormatter(s, "s").serialize(ast)
+    print("parser:\n" + str(parser))
+    match_objects = ParserCombinator(s, "s").get(parser)
+    pprint(match_objects)
-- 
GitLab