diff --git a/muppet/puppet/format/parser.py b/muppet/puppet/format/parser.py index ad28c5299df409f2eaee0f7b76dae73666c603e0..552483b004e8c3fc6202b3828322e8f3e71feb2f 100644 --- a/muppet/puppet/format/parser.py +++ b/muppet/puppet/format/parser.py @@ -444,13 +444,38 @@ class ParserFormatter(Serializer[ParseDirective]): @override def _puppet_hash(self, it: PuppetHash) -> ParseDirective: - parser = ws & '{' + """ + Parse a puppet hash literal. + + The reason for the enclosing braces being optional is that + they are in some contexts. + + For example: + + .. code-block:: puppet + + f("x", + a => 1, + b => 2) + + is parsed as + + .. code-block:: puppet + + f("x", { + a => 1, + b => 2 }) + + This will however not give any false positives, since our + parser is built from the source. + """ + parser = ws & optional(s('{')) for entry in it.entries: parser &= (ws & self.s(entry.k) & ws & '=>' & ws & self.s(entry.v) & optional(ws & ',')) - parser &= ws & '}' + parser &= ws & optional(s('}')) return parser @override diff --git a/tests/test_parse_elsif.py b/tests/test_parse_elsif.py index 02264153860141031a96640367f59feb815db72c..8de39f53e0a356ab878b242b115b71bbfdfa9aa8 100644 --- a/tests/test_parse_elsif.py +++ b/tests/test_parse_elsif.py @@ -29,19 +29,72 @@ error with the if block. import pytest from muppet.puppet.format.parser import ParserFormatter -from muppet.puppet.ast import build_ast +from muppet.puppet.ast import ( + build_ast, + Puppet, + HashEntry, + PuppetHash, + PuppetNumber, + PuppetQn, + PuppetString, + PuppetInvoke, +) from muppet.puppet.parser import puppet_parser from muppet.parser_combinator import ParserCombinator from pprint import pprint - - -def parse_string(s: str): - ast = build_ast(puppet_parser(s)) - pprint(ast) - parser = ParserFormatter(s, "s").serialize(ast) +from typing import Any, Optional + + +def parse_string(s: str, *, + ast: Optional[Puppet] = None, + matched: Optional[list[Any]] = None): + """ + Parse and validate the given puppet fragment. + + At it's core this procedure simply: + - Parses the puppet fragment into a well-behaved JSON structure + (note that this step is actually two steps, see documentation) + - Builds a Python AST from it + - Prints that AST to stdout + - Builds a parser combinator from that AST + - Prints that parser combinator to stdout + (TODO add rainbow parenthesis if module is available) + - Attempt to parse the original string with the generated parser + combinator + - Prints that result. + + After the generation and printing of different data, there's an + option to compare it to a known correct value. This comparison is + done inside an `assert`, meaning that it works well with the + running test. + + No comparison option exist for the parser object, since it may + contain functions, and the only function comparison is if it's the + exact same object, making it useless here. + + :param s: + A self contained puppet fragment. + :param ast: + AST to compare the output of `build_ast` to. + :param matched: + List of match objects (or whatever the parser combinator + returned) to compare agains. + """ + generated_ast = build_ast(puppet_parser(s)) + pprint(generated_ast) + if ast: + assert generated_ast == ast + + parser = ParserFormatter(s, "s").serialize(generated_ast) + + print() pprint(parser) match_objects = ParserCombinator(s, "s").get(parser) + + print() pprint(match_objects) + if matched: + assert matched == match_objects def test_parse_else_if(): @@ -121,6 +174,7 @@ def test_invoke_inside_if(): } """) + def test_funcall_with_block(): parse_string(""" assert_type(A::B, $name) |$_expected, $actual | { @@ -210,3 +264,25 @@ def test_unless(): } } """) + + +def test_bare_hash(): + # This will be parsed just as if there was a curly brace after + # `"String",`, with a matching end brace before the closing + # parenthesis. + s = """ + f("String", + x => 1, + y => 2, + ) + """ + parse_string( + s, + ast=PuppetInvoke( + PuppetQn(name='f'), + [PuppetString('String'), + PuppetHash(entries=[HashEntry(PuppetQn('x'), + PuppetNumber(1)), + HashEntry(PuppetQn('y'), + PuppetNumber(2))])])) + assert False