From d481985ce83d1e3f18c55f261d2fed2d344d4a29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se>
Date: Tue, 4 Jul 2023 01:55:04 +0200
Subject: [PATCH] Split puppet parse major match into multiple functions.

---
 muppet/format.py | 1200 +++++++++++++++++++++++-----------------------
 mypy.ini         |    3 -
 2 files changed, 598 insertions(+), 605 deletions(-)

diff --git a/muppet/format.py b/muppet/format.py
index 48d692c..734f8bd 100644
--- a/muppet/format.py
+++ b/muppet/format.py
@@ -171,459 +171,383 @@ def handle_case_body(forms: list[dict[str, Any]],
 # - qr
 # - var (except when it's the var declaration)
 
-LineFragment: TypeAlias = str | Tag
+LineFragment: TypeAlias = str | Markup
 Line: TypeAlias = list[LineFragment]
 
 
-def parse(form: Any, indent: int, context: list[str]) -> Markup:
-    """
-    Print everything from a puppet parse tree.
+def parse_access(how: Any, args: list[Any], *, indent: int, context: list[str]) -> Tag:
+    """Parse access form."""
+    # TODO newlines?
+    items = []
+    items += [parse(how, indent, context), '[']
+    for sublist in intersperse([',', ' '],
+                               [[parse(arg, indent, context)]
+                                for arg in args]):
+        items += sublist
+    items += [']']
+    return tag(items, 'access')
+
+
+def parse_array(items: list[Any], *, indent: int, context: list[str]) -> Tag:
+    """Parse array form."""
+    out: list[Markup]
+    out = ['[', '\n']
+    for item in items:
+        out += [
+            ind(indent+2),
+            parse(item, indent+1, context),
+            ',',
+            '\n',
+        ]
+    out += [ind(indent), ']']
+    return tag(out, 'array')
+
+
+def parse_call(func: Any, args: list[Any], *, indent: int, context: list[str]) -> Tag:
+    """Parse call form."""
+    items = []
+    items += [parse(func, indent, context), '(']
+    for sublist in intersperse([',', ' '],
+                               [[parse(arg, indent, context)]
+                                for arg in args]):
+        items += sublist
+    items += [')']
+    return tag(items, 'call')
+
+
+def parse_call_method(func: Any, *, indent: int, context: list[str]) -> Tag:
+    """Parse call method form."""
+    items = [parse(func['functor'], indent, context)]
+
+    if not ('block' in func and func['args'] == []):
+        items += ['(']
+        for sublist in intersperse([',', ' '],
+                                   [[parse(x, indent, context)]
+                                    for x in func['args']]):
+            items += sublist
+        items += [')']
 
-    :param from:
-        A puppet AST.
-    :param indent:
-        How many levels deep in indentation the code is.
-        Will get multiplied by the indentation width.
-    """
-    items: list[Markup]
-    # Sorted per `sort -V`
-    match form:
-        case None:
-            return tag('undef', 'literal', 'undef')
+    if 'block' in func:
+        items += [parse(func['block'], indent+1, context)]
 
-        case True:
-            return tag('true', 'literal', 'true')
+    return tag(items, 'call-method')
 
-        case False:
-            return tag('false', 'literal', 'false')
 
-        case ['access', how, *args]:
-            # TODO newlines?
-            items = []
-            items += [parse(how, indent, context), '[']
-            for sublist in intersperse([',', ' '],
-                                       [[parse(arg, indent, context)]
-                                        for arg in args]):
-                items += sublist
-            items += [']']
-            return tag(items, 'access')
+def parse_case(test: Any, forms: Any, *, indent: int, context: list[str]) -> Tag:
+    """Parse case form."""
+    items: list[Markup] = [
+        keyword('case'),
+        ' ',
+        parse(test, indent, context),
+        ' ', '{', '\n',
+        handle_case_body(forms, indent, context),
+        ind(indent),
+        '}',
+    ]
 
-        case ['and', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', keyword('and'), ' ',
-                parse(b, indent, context),
-            ])
+    return tag(items)
 
-        case ['array']:
-            return tag('[]', 'array')
 
-        case ['array', *items]:
-            out = ['[', '\n']
-            for item in items:
-                out += [
-                    ind(indent+2),
-                    parse(item, indent+1, context),
-                    ',',
-                    '\n',
+def parse_class(name: Any, rest: dict[str, Any],
+                *, indent: int, context: list[str]) -> Tag:
+    """Parse class form."""
+    items: list[Markup] = []
+    items += [
+        keyword('class'),
+        ' ',
+        tag(name, 'name'),
+        ' ',
+    ]
+
+    if 'params' in rest:
+        items += ['(', '\n']
+        for name, data in rest['params'].items():
+            decls: list[Markup] = []
+            decls += [ind(indent+1)]
+            if 'type' in data:
+                tt = parse(data['type'], indent+1, context)
+                decls += [tag(tt, 'type'),
+                          ' ']
+            decls += [declare_var(name)]
+            if 'value' in data:
+                decls += [
+                    ' ', operator('='), ' ',
+                    # TODO this is a declaration
+                    parse(data.get('value'), indent+1, context),
                 ]
-            out += [ind(indent), ']']
-            return tag(out, 'array')
-
-        case ['call', {'functor': func,
-                       'args': args}]:
-            items = []
-            items += [parse(func, indent, context), '(']
-            for sublist in intersperse([',', ' '],
-                                       [[parse(arg, indent, context)]
-                                        for arg in args]):
-                items += sublist
-            items += [')']
-            return tag(items, 'call')
-
-        case ['call-method', func]:
-            items = [parse(func['functor'], indent, context)]
-
-            if not ('block' in func and func['args'] == []):
-                items += ['(']
-                for sublist in intersperse([',', ' '],
-                                           [[parse(x, indent, context)]
-                                            for x in func['args']]):
-                    items += sublist
-                items += [')']
-
-            if 'block' in func:
-                items += [parse(func['block'], indent+1, context)]
+            items += [declaration(decls, 'declaration', variable=name)]
+            items += [',', '\n']
+        items += [ind(indent), ')', ' ', '{', '\n']
+    else:
+        items += ['{', '\n']
+
+    if 'body' in rest:
+        for entry in rest['body']:
+            items += [ind(indent+1),
+                      parse(entry, indent+1, context),
+                      '\n']
+    items += [ind(indent), '}']
+    return tag(items)
 
-            return tag(items, 'call-method')
 
-        case ['case', test, forms]:
-            items = [
-                keyword('case'),
-                ' ',
-                parse(test, indent, context),
-                ' ', '{', '\n',
-                handle_case_body(forms, indent, context),
-                ind(indent),
-                '}',
-            ]
-
-            return tag(items)
+def parse_concat(args: list[Any], *, indent: int, context: list[str]) -> Tag:
+    """Parse concat form."""
+    items = ['"']
+    for item in args:
+        match item:
+            case ['str', ['var', x]]:
+                items += [tag(['${', print_var(x, False), '}'], 'str-var')]
+            case ['str', thingy]:
+                content = parse(thingy, indent, ['str'] + context)
+                items += [tag(['${', content, '}'], 'str-var')]
+            case s:
+                items += [s
+                          .replace('"', '\\"')
+                          .replace('\n', '\\n')]
+    items += '"'
+    return tag(items, 'string')
+
+
+def parse_define(name: Any, rest: dict[str, Any],
+                 *, indent: int, context: list[str]) -> Tag:
+    """Parse define form."""
+    items: list[Markup] = []
+    items += [keyword('define'),
+              ' ',
+              tag(name, 'name'),
+              ' ']
+
+    if params := rest.get('params'):
+        items += ['(', '\n']
+        for name, data in params.items():
+            decl: list[Markup] = []
+            decl += [ind(indent+1)]
+            if 'type' in data:
+                decl += [tag(parse(data['type'], indent, context),
+                             'type'),
+                         ' ']
+            # print(f'<span class="var">${name}</span>', end='')
+            decl += [declare_var(name)]
+            if 'value' in data:
+                decl += [
+                    ' ', '=', ' ',
+                    parse(data.get('value'), indent, context),
+                ]
+            items += [declaration(decl, 'declaration', variable=name)]
+            items += [',', '\n']
 
-        case ['class', {'name': name,
-                        **rest}]:
-            items = []
-            items += [
-                keyword('class'),
-                ' ',
-                tag(name, 'name'),
-                ' ',
-            ]
+        items += [ind(indent), ')', ' ']
 
-            if 'params' in rest:
-                items += ['(', '\n']
-                for name, data in rest['params'].items():
-                    decls: list[Markup] = []
-                    decls += [ind(indent+1)]
-                    if 'type' in data:
-                        tt = parse(data['type'], indent+1, context)
-                        decls += [tag(tt, 'type'),
-                                  ' ']
-                    decls += [declare_var(name)]
-                    if 'value' in data:
-                        decls += [
-                            ' ', operator('='), ' ',
-                            # TODO this is a declaration
-                            parse(data.get('value'), indent+1, context),
-                        ]
-                    items += [declaration(decls, 'declaration', variable=name)]
-                    items += [',', '\n']
-                items += [ind(indent), ')', ' ', '{', '\n']
-            else:
-                items += ['{', '\n']
-
-            if 'body' in rest:
-                for entry in rest['body']:
-                    items += [ind(indent+1),
-                              parse(entry, indent+1, context),
-                              '\n']
-            items += [ind(indent), '}']
-            return tag(items)
+    items += ['{', '\n']
 
-        case ['concat', *args]:
-            items = ['"']
-            for item in args:
-                match item:
-                    case ['str', ['var', x]]:
-                        items += [tag(['${', print_var(x, False), '}'], 'str-var')]
-                    case ['str', thingy]:
-                        content = parse(thingy, indent, ['str'] + context)
-                        items += [tag(['${', content, '}'], 'str-var')]
-                    case s:
-                        items += [s
-                                  .replace('"', '\\"')
-                                  .replace('\n', '\\n')]
-            items += '"'
-            return tag(items, 'string')
-
-        case ['collect', {'type': t,
-                          'query': q}]:
-            return tag([parse(t, indent, context),
-                        ' ',
-                        parse(q, indent, context)])
+    if 'body' in rest:
+        for entry in rest['body']:
+            items += [ind(indent+1),
+                      parse(entry, indent+1, context),
+                      '\n']
 
-        case ['default']:
-            return keyword('default')
+    items += [ind(indent), '}']
 
-        case ['define', {'name': name,
-                         **rest}]:
-            items = [keyword('define'),
-                     ' ',
-                     tag(name, 'name'),
-                     ' ']
-
-            if params := rest.get('params'):
-                items += ['(', '\n']
-                for name, data in params.items():
-                    decl: list[Markup] = []
-                    decl += [ind(indent+1)]
-                    if 'type' in data:
-                        decl += [tag(parse(data['type'], indent, context),
-                                     'type'),
-                                 ' ']
-                    # print(f'<span class="var">${name}</span>', end='')
-                    decl += [declare_var(name)]
-                    if 'value' in data:
-                        decl += [
-                            ' ', '=', ' ',
-                            parse(data.get('value'), indent, context),
-                        ]
-                    items += [declaration(decl, 'declaration', variable=name)]
-                    items += [',', '\n']
+    return tag(items)
 
-                items += [ind(indent), ')', ' ']
 
-            items += ['{', '\n']
+def parse_function(name: Any, rest: dict[str, Any],
+                   *, indent: int, context: list[str]) -> Tag:
+    """Parse function form."""
+    items = []
+    items += [keyword('function'),
+              ' ', name]
+    if 'params' in rest:
+        items += [' ', '(', '\n']
+        for name, attributes in rest['params'].items():
+            items += [ind(indent+1)]
+            if 'type' in attributes:
+                items += [parse(attributes['type'], indent, context),
+                          ' ']
+            items += [f'${name}']
+            if 'value' in attributes:
+                items += [
+                    ' ', '=', ' ',
+                    parse(attributes['value'], indent, context),
+                ]
+            items += [',', '\n']
+        items += [ind(indent), ')']
 
-            if 'body' in rest:
-                for entry in rest['body']:
-                    items += [ind(indent+1),
-                              parse(entry, indent+1, context),
-                              '\n']
+    if 'returns' in rest:
+        items += [' ', '>>', ' ',
+                  parse(rest['returns'], indent, context)]
 
-            items += [ind(indent), '}']
+    items += [' ', '{']
+    if 'body' in rest:
+        items += ['\n']
+        for item in rest['body']:
+            items += [
+                ind(indent+1),
+                parse(item, indent+1, context),
+                '\n',
+            ]
+        items += [ind(indent)]
+    items += ['}']
+    return tag(items)
 
-            return tag(items)
 
-        case ['exported-query']:
-            return tag(['<<|', ' ', '|>>'])
+def parse_heredoc_concat(parts: list[Any],
+                         *, indent: int, context: list[str]) -> Tag:
+    """Parse heredoc form containing concatenation."""
+    items: list[Markup] = ['@("EOF")']
 
-        case ['exported-query', arg]:
-            return tag(['<<|', ' ',
-                        parse(arg, indent, context),
-                        ' ', '|>>'])
+    lines: list[Line] = [[]]
 
-        case ['function', {'name': name,
-                           **rest}]:
-            items = []
-            items += [keyword('function'),
-                      ' ', name]
-            if 'params' in rest:
-                items += [' ', '(', '\n']
-                for name, attributes in rest['params'].items():
-                    items += [ind(indent+1)]
-                    if 'type' in attributes:
-                        items += [parse(attributes['type'], indent, context),
-                                  ' ']
-                    items += [f'${name}']
-                    if 'value' in attributes:
-                        items += [
-                            ' ', '=', ' ',
-                            parse(attributes['value'], indent, context),
-                        ]
-                    items += [',', '\n']
-                items += [ind(indent), ')']
+    for part in parts:
+        match part:
+            case ['str', ['var', x]]:
+                lines[-1] += [tag(['${', print_var(x, False), '}'])]
+            case ['str', form]:
+                lines[-1] += [tag(['${', parse(form, indent, context), '}'])]
+            case s:
+                if not isinstance(s, str):
+                    raise ValueError('Unexpected value in heredoc', s)
 
-            if 'returns' in rest:
-                items += [' ', '>>', ' ',
-                          parse(rest['returns'], indent, context)]
+                first, *rest = s.split('\n')
+                lines[-1] += [first]
+                # lines += [[]]
 
-            items += [' ', '{']
-            if 'body' in rest:
-                items += ['\n']
-                for item in rest['body']:
-                    items += [
-                        ind(indent+1),
-                        parse(item, indent+1, context),
-                        '\n',
-                    ]
-                items += [ind(indent)]
-            items += ['}']
-            return tag(items)
+                for item1 in rest:
+                    lines += [[item1]]
 
-        case ['hash']:
-            return tag('{}', 'hash')
+    for line in lines:
+        items += ['\n']
+        if line != ['']:
+            items += [ind(indent)]
+            for item2 in line:
+                if item2:
+                    items += [item2]
 
-        case ['hash', *hash]:
-            return tag([
-                '{', '\n',
-                print_hash(hash, indent+1, context),
-                ind(indent),
-                '}',
-            ], 'hash')
+    match lines:
+        case [*_, ['']]:
+            # We have a trailing newline
+            items += [ind(indent), '|']
+        case _:
+            # We don't have a trailing newline
+            # Print the graphical one, but add the dash to the pipe
+            items += ['\n', ind(indent), '|-']
 
-        # TODO a safe string to use?
-        # TODO extra options?
-        #      Are all these already removed by the parser, requiring
-        #      us to reverse parse the text?
+    items += [' ', 'EOF']
+    return tag(items, 'heredoc', 'literal')
 
-        # Parts can NEVER be empty, since that case wouldn't generate
-        # a concat element, but a "plain" text element
-        case ['heredoc', {'text': ['concat', *parts]}]:
-            items = ['@("EOF")']
-
-            lines: list[Line] = [[]]
-
-            for part in parts:
-                match part:
-                    case ['str', ['var', x]]:
-                        lines[-1] += [tag(['${', print_var(x, False), '}'])]
-                    case ['str', form]:
-                        lines[-1] += [tag(['${', parse(form, indent, context), '}'])]
-                    case s:
-                        if not isinstance(s, str):
-                            raise ValueError('Unexpected value in heredoc', s)
-
-                        first, *rest = s.split('\n')
-                        lines[-1] += [first]
-                        # lines += [[]]
-
-                        for item in rest:
-                            lines += [[item]]
-
-            for line in lines:
-                items += ['\n']
-                if line != ['']:
-                    items += [ind(indent)]
-                    for item in line:
-                        if item:
-                            items += [item]
-
-            match lines:
-                case [*_, ['']]:
-                    # We have a trailing newline
-                    items += [ind(indent), '|']
-                case _:
-                    # We don't have a trailing newline
-                    # Print the graphical one, but add the dash to the pipe
-                    items += ['\n', ind(indent), '|-']
-
-            items += [' ', 'EOF']
-            return tag(items, 'heredoc', 'literal')
 
-        case ['heredoc', {'text': ''}]:
-            return tag(['@(EOF)', '\n', ind(indent), '|', ' ', 'EOF'],
-                       'heredoc', 'literal')
+def parse_heredoc_text(text: str, *, indent: int, context: list[str]) -> Tag:
+    """Parse heredoc form only containing text."""
+    items: list[Markup] = []
+    items += ['@(EOF)', '\n']
+    lines = text.split('\n')
 
-        case ['heredoc', {'text': text}]:
-            items = []
-            items += ['@(EOF)', '\n']
-            lines = text.split('\n')
+    no_eol: bool = True
 
-            no_eol: bool = True
+    if lines[-1] == '':
+        lines = lines[:-1]
+        no_eol = False
 
-            if lines[-1] == '':
-                lines = lines[:-1]
-                no_eol = False
+    for line in lines:
+        if line:
+            items += [ind(indent), line]
+        items += ['\n']
+    items += [ind(indent)]
 
-            for line in lines:
-                if line:
-                    items += [ind(indent), line]
-                items += ['\n']
-            items += [ind(indent)]
+    if no_eol:
+        items += ['|-']
+    else:
+        items += ['|']
+    items += [' ', 'EOF']
 
-            if no_eol:
-                items += ['|-']
-            else:
-                items += ['|']
-            items += [' ', 'EOF']
+    return tag(items, 'heredoc', 'literal')
 
-            return tag(items, 'heredoc', 'literal')
 
-        case ['if', {'test': test,
-                     **rest}]:
-            items = []
+def parse_if(test: Any, rest: dict[str, Any], *, indent: int, context: list[str]) -> Tag:
+    """Parse if form."""
+    items: list[Markup] = []
+    items += [
+        keyword('if'),
+        ' ',
+        parse(test, indent, context),
+        ' ', '{', '\n',
+    ]
+    if 'then' in rest:
+        for item in rest['then']:
             items += [
-                keyword('if'),
-                ' ',
-                parse(test, indent, context),
-                ' ', '{', '\n',
+                ind(indent+1),
+                parse(item, indent+1, context),
+                '\n',
             ]
-            if 'then' in rest:
-                for item in rest['then']:
+    items += [ind(indent), '}']
+
+    if 'else' in rest:
+        items += [' ']
+        match rest['else']:
+            case [['if', *rest]]:
+                # TODO propper tagging
+                items += ['els',
+                          parse(['if', *rest], indent, context)]
+            case el:
+                items += [keyword('else'),
+                          ' ', '{', '\n']
+                for item in el:
                     items += [
                         ind(indent+1),
                         parse(item, indent+1, context),
                         '\n',
                     ]
-            items += [ind(indent), '}']
-
-            if 'else' in rest:
-                items += [' ']
-                match rest['else']:
-                    case [['if', *rest]]:
-                        # TODO propper tagging
-                        items += ['els',
-                                  parse(['if', *rest], indent, context)]
-                    case el:
-                        items += [keyword('else'),
-                                  ' ', '{', '\n']
-                        for item in el:
-                            items += [
-                                ind(indent+1),
-                                parse(item, indent+1, context),
-                                '\n',
-                            ]
-                        items += [
-                            ind(indent),
-                            '}',
-                        ]
-            return tag(items)
-
-        case ['in', needle, stack]:
-            return tag([
-                parse(needle, indent, context),
-                ' ', keyword('in'), ' ',
-                parse(stack, indent, context),
-            ])
-
-        case ['invoke', {'functor': func,
-                         'args': args}]:
-            items = [
-                parse(func, indent, context),
-                ' ',
-            ]
-            if len(args) == 1:
-                items += [parse(args[0], indent+1, context)]
-            else:
-                items += ['(']
-                for sublist in intersperse([',', ' '],
-                                           [[parse(arg, indent+1, context)]
-                                            for arg in args]):
-                    items += sublist
-                items += [')']
-            return tag(items, 'invoke')
-
-        case ['nop']:
-            return tag('', 'nop')
-
-        case ['lambda', {'params': params,
-                         'body': body}]:
-            items = []
-            # TODO note these are declarations
-            items += ['|']
-            for sublist in intersperse([',', ' '],
-                                       [[f'${x}'] for x in params.keys()]):
-                items += sublist
-            items += ['|', ' ', '{', '\n']
-            for entry in body:
                 items += [
                     ind(indent),
-                    parse(entry, indent, context),
-                    '\n',
+                    '}',
                 ]
-            items += [ind(indent-1), '}']
-            return tag(items, 'lambda')
-
-        case ['or', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', keyword('or'), ' ',
-                parse(b, indent, context),
-            ])
-
-        case ['paren', *forms]:
-            return tag([
-                '(',
-                *(parse(form, indent+1, context)
-                  for form in forms),
-                ')',
-            ], 'paren')
+    return tag(items)
 
-        # Qualified name?
-        case ['qn', x]:
-            return tag(x, 'qn')
 
-        # Qualified resource?
-        case ['qr', x]:
-            return tag(x, 'qr')
+def parse_invoke(func: Any, args: list[Any],
+                 *, indent: int, context: list[str]) -> Tag:
+    """Parse invoke form."""
+    items = [
+        parse(func, indent, context),
+        ' ',
+    ]
+    if len(args) == 1:
+        items += [parse(args[0], indent+1, context)]
+    else:
+        items += ['(']
+        for sublist in intersperse([',', ' '],
+                                   [[parse(arg, indent+1, context)]
+                                    for arg in args]):
+            items += sublist
+        items += [')']
+    return tag(items, 'invoke')
 
-        case ['regexp', s]:
-            return tag(['/', tag(s, 'regex-body'), '/'], 'regex')
 
-        # Resource instansiation with exactly one instance
-        case ['resource', {'type': t,
-                           'bodies': [body]}]:
+def parse_lambda(params: dict[str, Any], body: Any,
+                 *, indent: int, context: list[str]) -> Tag:
+    """Parse lambda form."""
+    items: list[Markup] = []
+    # TODO note these are declarations
+    items += ['|']
+    for sublist in intersperse([',', ' '],
+                               [[f'${x}'] for x in params.keys()]):
+        items += sublist
+    items += ['|', ' ', '{', '\n']
+    for entry in body:
+        items += [
+            ind(indent),
+            parse(entry, indent, context),
+            '\n',
+        ]
+    items += [ind(indent-1), '}']
+    return tag(items, 'lambda')
+
+
+def parse_resource(t: str, bodies: list[Any],
+                   *, indent: int, context: list[str]) -> Tag:
+    """Parse resource form."""
+    match bodies:
+        case [body]:
             items = [
                 parse(t, indent, context),
                 ' ', '{', ' ',
@@ -665,10 +589,7 @@ def parse(form: Any, indent: int, context: list[str]) -> Markup:
             ]
 
             return tag(items)
-
-        # Resource instansiation with any number of instances
-        case ['resource', {'type': t,
-                           'bodies': bodies}]:
+        case bodies:
             items = []
             items += [
                 parse(t, indent, context),
@@ -714,227 +635,330 @@ def parse(form: Any, indent: int, context: list[str]) -> Markup:
             items += ['\n', ind(indent), '}']
             return tag(items)
 
-        case ['resource-defaults', {'type': t,
-                                    'ops': ops}]:
-            items = [
-                parse(t, indent, context),
-                ' ', '{', '\n',
-            ]
-            namelen = ops_namelen(ops)
-            for op in ops:
-                match op:
-                    case ['=>', key, value]:
-                        pad = namelen - len(key)
-                        items += [
-                            ind(indent+1),
-                            tag(key, 'parameter'),
-                            ' '*pad,
-                            ' ', operator('=>'), ' ',
-                            parse(value, indent+3, context),
-                            ',', '\n',
-                        ]
 
-                    case ['splat-hash', value]:
-                        pad = namelen - 1
-                        items += [
-                            ind(indent+1),
-                            tag('*', 'parameter', 'splat'),
-                            ' '*pad,
-                            ' ', operator('=>'), ' ',
-                            parse(value, indent+2, context),
-                            ',', '\n',
-                        ]
+def parse_resource_defaults(t: str, ops: Any,
+                            *, indent: int, context: list[str]) -> Tag:
+    """Parse resource defaults form."""
+    items = [
+        parse(t, indent, context),
+        ' ', '{', '\n',
+    ]
+    namelen = ops_namelen(ops)
+    for op in ops:
+        match op:
+            case ['=>', key, value]:
+                pad = namelen - len(key)
+                items += [
+                    ind(indent+1),
+                    tag(key, 'parameter'),
+                    ' '*pad,
+                    ' ', operator('=>'), ' ',
+                    parse(value, indent+3, context),
+                    ',', '\n',
+                ]
 
-                    case x:
-                        raise Exception('Unexpected item in resource defaults:', x)
+            case ['splat-hash', value]:
+                pad = namelen - 1
+                items += [
+                    ind(indent+1),
+                    tag('*', 'parameter', 'splat'),
+                    ' '*pad,
+                    ' ', operator('=>'), ' ',
+                    parse(value, indent+2, context),
+                    ',', '\n',
+                ]
 
-            items += [ind(indent),
-                      '}']
+            case x:
+                raise Exception('Unexpected item in resource defaults:', x)
 
-            return tag(items)
+    items += [ind(indent),
+              '}']
 
-        case ['resource-override', {'resources': resources,
-                                    'ops': ops}]:
-            items = [
-                parse(resources, indent, context),
-                ' ', '{', '\n',
-            ]
+    return tag(items)
 
-            namelen = ops_namelen(ops)
-            for op in ops:
-                match op:
-                    case ['=>', key, value]:
-                        pad = namelen - len(key)
-                        items += [
-                            ind(indent+1),
-                            tag(key, 'parameter'),
-                            ' '*pad,
-                            ' ', operator('=>'), ' ',
-                            parse(value, indent+3, context),
-                            ',', '\n',
-                        ]
 
-                    case ['+>', key, value]:
-                        pad = namelen - len(key)
-                        items += [
-                            ind(indent+1),
-                            tag(key, 'parameter'),
-                            ' '*pad,
-                            ' ', operator('+>'), ' ',
-                            parse(value, indent+2, context),
-                            ',', '\n',
-                        ]
+def parse_resource_override(resources: Any, ops: Any,
+                            *, indent: int, context: list[str]) -> Tag:
+    """Parse resoruce override form."""
+    items = [
+        parse(resources, indent, context),
+        ' ', '{', '\n',
+    ]
 
-                    case ['splat-hash', value]:
-                        pad = namelen - 1
-                        items += [
-                            ind(indent+1),
-                            tag('*', 'parameter', 'splat'),
-                            ' '*pad,
-                            ' ', operator('=>'), ' ',
-                            parse(value, indent+2, context),
-                            ',', '\n',
-                        ]
+    namelen = ops_namelen(ops)
+    for op in ops:
+        match op:
+            case ['=>', key, value]:
+                pad = namelen - len(key)
+                items += [
+                    ind(indent+1),
+                    tag(key, 'parameter'),
+                    ' '*pad,
+                    ' ', operator('=>'), ' ',
+                    parse(value, indent+3, context),
+                    ',', '\n',
+                ]
 
-                    case _:
-                        raise Exception('Unexpected item in resource override:',
-                                        op)
+            case ['+>', key, value]:
+                pad = namelen - len(key)
+                items += [
+                    ind(indent+1),
+                    tag(key, 'parameter'),
+                    ' '*pad,
+                    ' ', operator('+>'), ' ',
+                    parse(value, indent+2, context),
+                    ',', '\n',
+                ]
 
-            items += [
-                ind(indent),
-                '}',
-            ]
+            case ['splat-hash', value]:
+                pad = namelen - 1
+                items += [
+                    ind(indent+1),
+                    tag('*', 'parameter', 'splat'),
+                    ' '*pad,
+                    ' ', operator('=>'), ' ',
+                    parse(value, indent+2, context),
+                    ',', '\n',
+                ]
 
-            return tag(items)
+            case _:
+                raise Exception('Unexpected item in resource override:',
+                                op)
 
-        case ['unless', {'test': test,
-                         **rest}]:
-            items = [
-                keyword('unless'),
-                ' ',
-                parse(test, indent, context),
-                ' ', '{', '\n',
-            ]
+    items += [
+        ind(indent),
+        '}',
+    ]
 
-            if 'then' in rest:
-                for item in rest['then']:
-                    items += [
-                        ind(indent+1),
-                        parse(item, indent+1, context),
-                        '\n',
-                    ]
+    return tag(items)
+
+
+def parse_unless(test: Any, rest: dict[str, Any],
+                 *, indent: int, context: list[str]) -> Tag:
+    """Parse unless form."""
+    items: list[Markup] = [
+        keyword('unless'),
+        ' ',
+        parse(test, indent, context),
+        ' ', '{', '\n',
+    ]
 
+    if 'then' in rest:
+        for item in rest['then']:
             items += [
-                ind(indent),
-                '}',
+                ind(indent+1),
+                parse(item, indent+1, context),
+                '\n',
             ]
-            return tag(items)
 
-        case ['var', x]:
-            if context[0] == 'declaration':
-                return declare_var(x)
-            else:
-                return print_var(x, True)
+    items += [
+        ind(indent),
+        '}',
+    ]
+    return tag(items)
 
-        case ['virtual-query', q]:
-            return tag([
-                '<|', ' ',
-                parse(q, indent, context),
-                ' ', '|>',
-            ])
 
-        case ['virtual-query']:
-            return tag(['<|', ' ', '|>'])
+def parse_operator(op: str, lhs: Any, rhs: Any,
+                   *, indent: int, context: list[str]) -> Tag:
+    """Parse binary generic operator form."""
+    return tag([
+        parse(lhs, indent, context),
+        ' ', operator(op), ' ',
+        parse(rhs, indent, context),
+    ])
 
-        case ['!', x]:
-            return tag([
-                operator('!'), ' ',
-                parse(x, indent, context),
-            ])
 
-        case ['!=', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('!='), ' ',
-                parse(b, indent, context),
-            ])
+def parse(form: Any, indent: int, context: list[str]) -> Markup:
+    """
+    Print everything from a puppet parse tree.
 
-        case ['+', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('+'), ' ',
-                parse(b, indent, context),
-            ])
+    :param from:
+        A puppet AST.
+    :param indent:
+        How many levels deep in indentation the code is.
+        Will get multiplied by the indentation width.
+    """
+    items: list[Markup]
+    # Sorted per `sort -V`
+    match form:
+        case None:
+            return tag('undef', 'literal', 'undef')
 
-        case ['-', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('-'), ' ',
-                parse(b, indent, context),
-            ])
+        case True:
+            return tag('true', 'literal', 'true')
 
-        case ['-', a]:
-            return tag([
-                operator('-'), ' ',
-                parse(a, indent, context),
-            ])
+        case False:
+            return tag('false', 'literal', 'false')
 
-        case ['*', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('*'), ' ',
-                parse(b, indent, context),
-            ])
+        case ['access', how, *args]:
+            return parse_access(how, args, indent=indent, context=context)
 
-        case ['%', a, b]:
+        case ['and', a, b]:
             return tag([
                 parse(a, indent, context),
-                ' ', operator('%'), ' ',
+                ' ', keyword('and'), ' ',
                 parse(b, indent, context),
             ])
 
-        case ['<<', a, b]:
+        case ['array']:
+            return tag('[]', 'array')
+
+        case ['array', *items]:
+            return parse_array(items, indent=indent, context=context)
+
+        case ['call', {'functor': func, 'args': args}]:
+            return parse_call(func, args, indent=indent, context=context)
+
+        case ['call-method', func]:
+            return parse_call_method(func, indent=indent, context=context)
+
+        case ['case', test, forms]:
+            return parse_case(test, forms, indent=indent, context=context)
+
+        case ['class', {'name': name, **rest}]:
+            return parse_class(name, rest, indent=indent, context=context)
+
+        case ['concat', *args]:
+            return parse_concat(args, indent=indent, context=context)
+
+        case ['collect', {'type': t, 'query': q}]:
+            return tag([parse(t, indent, context),
+                        ' ',
+                        parse(q, indent, context)])
+
+        case ['default']:
+            return keyword('default')
+
+        case ['define', {'name': name, **rest}]:
+            return parse_define(name, rest, indent=indent, context=context)
+
+        case ['exported-query']:
+            return tag(['<<|', ' ', '|>>'])
+
+        case ['exported-query', arg]:
+            return tag(['<<|', ' ', parse(arg, indent, context), ' ', '|>>'])
+
+        case ['function', {'name': name, **rest}]:
+            return parse_function(name, rest, indent=indent, context=context)
+
+        case ['hash']:
+            return tag('{}', 'hash')
+
+        case ['hash', *hash]:
             return tag([
-                parse(a, indent, context),
-                ' ', operator('<<'), ' ',
-                parse(b, indent, context),
-            ])
+                '{', '\n',
+                print_hash(hash, indent+1, context),
+                ind(indent),
+                '}',
+            ], 'hash')
+
+        # TODO a safe string to use?
+        # TODO extra options?
+        #      Are all these already removed by the parser, requiring
+        #      us to reverse parse the text?
+
+        # Parts can NEVER be empty, since that case wouldn't generate
+        # a concat element, but a "plain" text element
+        case ['heredoc', {'text': ['concat', *parts]}]:
+            return parse_heredoc_concat(parts, indent=indent, context=context)
+
+        case ['heredoc', {'text': ''}]:
+            return tag(['@(EOF)', '\n', ind(indent), '|', ' ', 'EOF'],
+                       'heredoc', 'literal')
+
+        case ['heredoc', {'text': text}]:
+            return parse_heredoc_text(text, indent=indent, context=context)
+
+        case ['if', {'test': test, **rest}]:
+            return parse_if(test, rest, indent=indent, context=context)
 
-        case ['>>', a, b]:
+        case ['in', needle, stack]:
             return tag([
-                parse(a, indent, context),
-                ' ', operator('>>'), ' ',
-                parse(b, indent, context),
+                parse(needle, indent, context),
+                ' ', keyword('in'), ' ',
+                parse(stack, indent, context),
             ])
 
-        case ['>=', a, b]:
+        case ['invoke', {'functor': func, 'args': args}]:
+            return parse_invoke(func, args, indent=indent, context=context)
+
+        case ['nop']:
+            return tag('', 'nop')
+
+        case ['lambda', {'params': params, 'body': body}]:
+            return parse_lambda(params, body, indent=indent, context=context)
+
+        case ['or', a, b]:
             return tag([
                 parse(a, indent, context),
-                ' ', operator('>='), ' ',
+                ' ', keyword('or'), ' ',
                 parse(b, indent, context),
             ])
 
-        case ['<=', a, b]:
+        case ['paren', *forms]:
             return tag([
-                parse(a, indent, context),
-                ' ', operator('<='), ' ',
-                parse(b, indent, context),
-            ])
+                '(',
+                *(parse(form, indent+1, context)
+                  for form in forms),
+                ')',
+            ], 'paren')
+
+        # Qualified name?
+        case ['qn', x]:
+            return tag(x, 'qn')
 
-        case ['>', a, b]:
+        # Qualified resource?
+        case ['qr', x]:
+            return tag(x, 'qr')
+
+        case ['regexp', s]:
+            return tag(['/', tag(s, 'regex-body'), '/'], 'regex')
+
+        # Resource instansiation with exactly one instance
+        case ['resource', {'type': t, 'bodies': [body]}]:
+            return parse_resource(t, [body], indent=indent, context=context)
+
+        # Resource instansiation with any number of instances
+        case ['resource', {'type': t, 'bodies': bodies}]:
+            return parse_resource(t, bodies, indent=indent, context=context)
+
+        case ['resource-defaults', {'type': t, 'ops': ops}]:
+            return parse_resource_defaults(t, ops, indent=indent, context=context)
+
+        case ['resource-override', {'resources': resources, 'ops': ops}]:
+            return parse_resource_override(resources, ops, indent=indent, context=context)
+
+        case ['unless', {'test': test, **rest}]:
+            return parse_unless(test, rest, indent=indent, context=context)
+
+        case ['var', x]:
+            if context[0] == 'declaration':
+                return declare_var(x)
+            else:
+                return print_var(x, True)
+
+        case ['virtual-query', q]:
+            return tag(['<|', ' ', parse(q, indent, context), ' ', '|>', ])
+
+        case ['virtual-query']:
+            return tag(['<|', ' ', '|>'])
+
+        case ['!', x]:
             return tag([
-                parse(a, indent, context),
-                ' ', operator('>'), ' ',
-                parse(b, indent, context),
+                operator('!'), ' ',
+                parse(x, indent, context),
             ])
 
-        case ['<', a, b]:
+        case ['-', a]:
             return tag([
+                operator('-'), ' ',
                 parse(a, indent, context),
-                ' ', operator('<'), ' ',
-                parse(b, indent, context),
             ])
 
+        case [('!=' | '+' | '-' | '*' | '%' | '<<' | '>>' | '>=' | '<=' | '>' | '<' | '/' | '==' | '=~' | '!~') as op,  # noqa: E501
+              a, b]:
+            return parse_operator(op, a, b, indent=indent, context=context)
+
         case ['~>', left, right]:
             return tag([
                 parse(left, indent, context),
@@ -962,13 +986,6 @@ def parse(form: Any, indent: int, context: list[str]) -> Markup:
                 parse(right, indent+1, context),
             ])
 
-        case ['/', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('/'), ' ',
-                parse(b, indent, context),
-            ])
-
         case ['=', field, value]:
             return tag([
                 parse(field, indent, ['declaration'] + context),
@@ -976,27 +993,6 @@ def parse(form: Any, indent: int, context: list[str]) -> Markup:
                 parse(value, indent, context),
             ], 'declaration')
 
-        case ['==', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('=='), ' ',
-                parse(b, indent, context),
-            ])
-
-        case ['=~', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('=~'), ' ',
-                parse(b, indent, context),
-            ])
-
-        case ['!~', a, b]:
-            return tag([
-                parse(a, indent, context),
-                ' ', operator('!~'), ' ',
-                parse(b, indent, context),
-            ])
-
         case ['?', condition, cases]:
             return tag([
                 parse(condition, indent, context),
diff --git a/mypy.ini b/mypy.ini
index 7c7a251..0d2369b 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,7 +1,4 @@
 [mypy]
-# Disabled since `match` breaks it.
-disable_error_code = used-before-def
-
 disallow_untyped_calls = True
 disallow_untyped_defs = True
 disallow_incomplete_defs = True
-- 
GitLab