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