1# Copyright (c) 2009-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
2# Copyright (c) 2010 Daniel Harding <dharding@gmail.com>
3# Copyright (c) 2013-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com>
4# Copyright (c) 2013-2014 Google, Inc.
5# Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
6# Copyright (c) 2016 Jared Garst <jgarst@users.noreply.github.com>
7# Copyright (c) 2016 Jakub Wilk <jwilk@jwilk.net>
8# Copyright (c) 2017, 2019 Łukasz Rogalski <rogalski.91@gmail.com>
9# Copyright (c) 2017 rr- <rr-@sakuya.pl>
10# Copyright (c) 2018 Serhiy Storchaka <storchaka@gmail.com>
11# Copyright (c) 2018 Ville Skyttä <ville.skytta@iki.fi>
12# Copyright (c) 2018 brendanator <brendan.maginnis@gmail.com>
13# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
14# Copyright (c) 2019 Alex Hall <alex.mojaki@gmail.com>
15# Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com>
16# Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
17# Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
18# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
19# Copyright (c) 2021 pre-commit-ci[bot] <bot@noreply.github.com>
20
21# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
22# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
23
24"""This module renders Astroid nodes as string"""
25from typing import TYPE_CHECKING, List
26
27if TYPE_CHECKING:
28    from astroid.nodes.node_classes import (
29        Match,
30        MatchAs,
31        MatchCase,
32        MatchClass,
33        MatchMapping,
34        MatchOr,
35        MatchSequence,
36        MatchSingleton,
37        MatchStar,
38        MatchValue,
39        Unknown,
40    )
41
42# pylint: disable=unused-argument
43
44DOC_NEWLINE = "\0"
45
46
47# Visitor pattern require argument all the time and is not better with staticmethod
48# noinspection PyUnusedLocal,PyMethodMayBeStatic
49class AsStringVisitor:
50    """Visitor to render an Astroid node as a valid python code string"""
51
52    def __init__(self, indent="    "):
53        self.indent = indent
54
55    def __call__(self, node):
56        """Makes this visitor behave as a simple function"""
57        return node.accept(self).replace(DOC_NEWLINE, "\n")
58
59    def _docs_dedent(self, doc):
60        """Stop newlines in docs being indented by self._stmt_list"""
61        return '\n{}"""{}"""'.format(self.indent, doc.replace("\n", DOC_NEWLINE))
62
63    def _stmt_list(self, stmts, indent=True):
64        """return a list of nodes to string"""
65        stmts = "\n".join(nstr for nstr in [n.accept(self) for n in stmts] if nstr)
66        if indent:
67            return self.indent + stmts.replace("\n", "\n" + self.indent)
68
69        return stmts
70
71    def _precedence_parens(self, node, child, is_left=True):
72        """Wrap child in parens only if required to keep same semantics"""
73        if self._should_wrap(node, child, is_left):
74            return f"({child.accept(self)})"
75
76        return child.accept(self)
77
78    def _should_wrap(self, node, child, is_left):
79        """Wrap child if:
80        - it has lower precedence
81        - same precedence with position opposite to associativity direction
82        """
83        node_precedence = node.op_precedence()
84        child_precedence = child.op_precedence()
85
86        if node_precedence > child_precedence:
87            # 3 * (4 + 5)
88            return True
89
90        if (
91            node_precedence == child_precedence
92            and is_left != node.op_left_associative()
93        ):
94            # 3 - (4 - 5)
95            # (2**3)**4
96            return True
97
98        return False
99
100    # visit_<node> methods ###########################################
101
102    def visit_await(self, node):
103        return f"await {node.value.accept(self)}"
104
105    def visit_asyncwith(self, node):
106        return f"async {self.visit_with(node)}"
107
108    def visit_asyncfor(self, node):
109        return f"async {self.visit_for(node)}"
110
111    def visit_arguments(self, node):
112        """return an astroid.Function node as string"""
113        return node.format_args()
114
115    def visit_assignattr(self, node):
116        """return an astroid.AssAttr node as string"""
117        return self.visit_attribute(node)
118
119    def visit_assert(self, node):
120        """return an astroid.Assert node as string"""
121        if node.fail:
122            return f"assert {node.test.accept(self)}, {node.fail.accept(self)}"
123        return f"assert {node.test.accept(self)}"
124
125    def visit_assignname(self, node):
126        """return an astroid.AssName node as string"""
127        return node.name
128
129    def visit_assign(self, node):
130        """return an astroid.Assign node as string"""
131        lhs = " = ".join(n.accept(self) for n in node.targets)
132        return f"{lhs} = {node.value.accept(self)}"
133
134    def visit_augassign(self, node):
135        """return an astroid.AugAssign node as string"""
136        return f"{node.target.accept(self)} {node.op} {node.value.accept(self)}"
137
138    def visit_annassign(self, node):
139        """Return an astroid.AugAssign node as string"""
140
141        target = node.target.accept(self)
142        annotation = node.annotation.accept(self)
143        if node.value is None:
144            return f"{target}: {annotation}"
145        return f"{target}: {annotation} = {node.value.accept(self)}"
146
147    def visit_binop(self, node):
148        """return an astroid.BinOp node as string"""
149        left = self._precedence_parens(node, node.left)
150        right = self._precedence_parens(node, node.right, is_left=False)
151        if node.op == "**":
152            return f"{left}{node.op}{right}"
153
154        return f"{left} {node.op} {right}"
155
156    def visit_boolop(self, node):
157        """return an astroid.BoolOp node as string"""
158        values = [f"{self._precedence_parens(node, n)}" for n in node.values]
159        return (f" {node.op} ").join(values)
160
161    def visit_break(self, node):
162        """return an astroid.Break node as string"""
163        return "break"
164
165    def visit_call(self, node):
166        """return an astroid.Call node as string"""
167        expr_str = self._precedence_parens(node, node.func)
168        args = [arg.accept(self) for arg in node.args]
169        if node.keywords:
170            keywords = [kwarg.accept(self) for kwarg in node.keywords]
171        else:
172            keywords = []
173
174        args.extend(keywords)
175        return f"{expr_str}({', '.join(args)})"
176
177    def visit_classdef(self, node):
178        """return an astroid.ClassDef node as string"""
179        decorate = node.decorators.accept(self) if node.decorators else ""
180        args = [n.accept(self) for n in node.bases]
181        if node._metaclass and not node.has_metaclass_hack():
182            args.append("metaclass=" + node._metaclass.accept(self))
183        args += [n.accept(self) for n in node.keywords]
184        args = f"({', '.join(args)})" if args else ""
185        docs = self._docs_dedent(node.doc) if node.doc else ""
186        return "\n\n{}class {}{}:{}\n{}\n".format(
187            decorate, node.name, args, docs, self._stmt_list(node.body)
188        )
189
190    def visit_compare(self, node):
191        """return an astroid.Compare node as string"""
192        rhs_str = " ".join(
193            f"{op} {self._precedence_parens(node, expr, is_left=False)}"
194            for op, expr in node.ops
195        )
196        return f"{self._precedence_parens(node, node.left)} {rhs_str}"
197
198    def visit_comprehension(self, node):
199        """return an astroid.Comprehension node as string"""
200        ifs = "".join(f" if {n.accept(self)}" for n in node.ifs)
201        generated = f"for {node.target.accept(self)} in {node.iter.accept(self)}{ifs}"
202        return f"{'async ' if node.is_async else ''}{generated}"
203
204    def visit_const(self, node):
205        """return an astroid.Const node as string"""
206        if node.value is Ellipsis:
207            return "..."
208        return repr(node.value)
209
210    def visit_continue(self, node):
211        """return an astroid.Continue node as string"""
212        return "continue"
213
214    def visit_delete(self, node):  # XXX check if correct
215        """return an astroid.Delete node as string"""
216        return f"del {', '.join(child.accept(self) for child in node.targets)}"
217
218    def visit_delattr(self, node):
219        """return an astroid.DelAttr node as string"""
220        return self.visit_attribute(node)
221
222    def visit_delname(self, node):
223        """return an astroid.DelName node as string"""
224        return node.name
225
226    def visit_decorators(self, node):
227        """return an astroid.Decorators node as string"""
228        return "@%s\n" % "\n@".join(item.accept(self) for item in node.nodes)
229
230    def visit_dict(self, node):
231        """return an astroid.Dict node as string"""
232        return "{%s}" % ", ".join(self._visit_dict(node))
233
234    def _visit_dict(self, node):
235        for key, value in node.items:
236            key = key.accept(self)
237            value = value.accept(self)
238            if key == "**":
239                # It can only be a DictUnpack node.
240                yield key + value
241            else:
242                yield f"{key}: {value}"
243
244    def visit_dictunpack(self, node):
245        return "**"
246
247    def visit_dictcomp(self, node):
248        """return an astroid.DictComp node as string"""
249        return "{{{}: {} {}}}".format(
250            node.key.accept(self),
251            node.value.accept(self),
252            " ".join(n.accept(self) for n in node.generators),
253        )
254
255    def visit_expr(self, node):
256        """return an astroid.Discard node as string"""
257        return node.value.accept(self)
258
259    def visit_emptynode(self, node):
260        """dummy method for visiting an Empty node"""
261        return ""
262
263    def visit_excepthandler(self, node):
264        if node.type:
265            if node.name:
266                excs = f"except {node.type.accept(self)} as {node.name.accept(self)}"
267            else:
268                excs = f"except {node.type.accept(self)}"
269        else:
270            excs = "except"
271        return f"{excs}:\n{self._stmt_list(node.body)}"
272
273    def visit_empty(self, node):
274        """return an Empty node as string"""
275        return ""
276
277    def visit_for(self, node):
278        """return an astroid.For node as string"""
279        fors = "for {} in {}:\n{}".format(
280            node.target.accept(self), node.iter.accept(self), self._stmt_list(node.body)
281        )
282        if node.orelse:
283            fors = f"{fors}\nelse:\n{self._stmt_list(node.orelse)}"
284        return fors
285
286    def visit_importfrom(self, node):
287        """return an astroid.ImportFrom node as string"""
288        return "from {} import {}".format(
289            "." * (node.level or 0) + node.modname, _import_string(node.names)
290        )
291
292    def visit_joinedstr(self, node):
293        string = "".join(
294            # Use repr on the string literal parts
295            # to get proper escapes, e.g. \n, \\, \"
296            # But strip the quotes off the ends
297            # (they will always be one character: ' or ")
298            repr(value.value)[1:-1]
299            # Literal braces must be doubled to escape them
300            .replace("{", "{{").replace("}", "}}")
301            # Each value in values is either a string literal (Const)
302            # or a FormattedValue
303            if type(value).__name__ == "Const" else value.accept(self)
304            for value in node.values
305        )
306
307        # Try to find surrounding quotes that don't appear at all in the string.
308        # Because the formatted values inside {} can't contain backslash (\)
309        # using a triple quote is sometimes necessary
310        for quote in ("'", '"', '"""', "'''"):
311            if quote not in string:
312                break
313
314        return "f" + quote + string + quote
315
316    def visit_formattedvalue(self, node):
317        result = node.value.accept(self)
318        if node.conversion and node.conversion >= 0:
319            # e.g. if node.conversion == 114: result += "!r"
320            result += "!" + chr(node.conversion)
321        if node.format_spec:
322            # The format spec is itself a JoinedString, i.e. an f-string
323            # We strip the f and quotes of the ends
324            result += ":" + node.format_spec.accept(self)[2:-1]
325        return "{%s}" % result
326
327    def handle_functiondef(self, node, keyword):
328        """return a (possibly async) function definition node as string"""
329        decorate = node.decorators.accept(self) if node.decorators else ""
330        docs = self._docs_dedent(node.doc) if node.doc else ""
331        trailer = ":"
332        if node.returns:
333            return_annotation = " -> " + node.returns.as_string()
334            trailer = return_annotation + ":"
335        def_format = "\n%s%s %s(%s)%s%s\n%s"
336        return def_format % (
337            decorate,
338            keyword,
339            node.name,
340            node.args.accept(self),
341            trailer,
342            docs,
343            self._stmt_list(node.body),
344        )
345
346    def visit_functiondef(self, node):
347        """return an astroid.FunctionDef node as string"""
348        return self.handle_functiondef(node, "def")
349
350    def visit_asyncfunctiondef(self, node):
351        """return an astroid.AsyncFunction node as string"""
352        return self.handle_functiondef(node, "async def")
353
354    def visit_generatorexp(self, node):
355        """return an astroid.GeneratorExp node as string"""
356        return "({} {})".format(
357            node.elt.accept(self), " ".join(n.accept(self) for n in node.generators)
358        )
359
360    def visit_attribute(self, node):
361        """return an astroid.Getattr node as string"""
362        left = self._precedence_parens(node, node.expr)
363        if left.isdigit():
364            left = f"({left})"
365        return f"{left}.{node.attrname}"
366
367    def visit_global(self, node):
368        """return an astroid.Global node as string"""
369        return f"global {', '.join(node.names)}"
370
371    def visit_if(self, node):
372        """return an astroid.If node as string"""
373        ifs = [f"if {node.test.accept(self)}:\n{self._stmt_list(node.body)}"]
374        if node.has_elif_block():
375            ifs.append(f"el{self._stmt_list(node.orelse, indent=False)}")
376        elif node.orelse:
377            ifs.append(f"else:\n{self._stmt_list(node.orelse)}")
378        return "\n".join(ifs)
379
380    def visit_ifexp(self, node):
381        """return an astroid.IfExp node as string"""
382        return "{} if {} else {}".format(
383            self._precedence_parens(node, node.body, is_left=True),
384            self._precedence_parens(node, node.test, is_left=True),
385            self._precedence_parens(node, node.orelse, is_left=False),
386        )
387
388    def visit_import(self, node):
389        """return an astroid.Import node as string"""
390        return f"import {_import_string(node.names)}"
391
392    def visit_keyword(self, node):
393        """return an astroid.Keyword node as string"""
394        if node.arg is None:
395            return f"**{node.value.accept(self)}"
396        return f"{node.arg}={node.value.accept(self)}"
397
398    def visit_lambda(self, node):
399        """return an astroid.Lambda node as string"""
400        args = node.args.accept(self)
401        body = node.body.accept(self)
402        if args:
403            return f"lambda {args}: {body}"
404
405        return f"lambda: {body}"
406
407    def visit_list(self, node):
408        """return an astroid.List node as string"""
409        return f"[{', '.join(child.accept(self) for child in node.elts)}]"
410
411    def visit_listcomp(self, node):
412        """return an astroid.ListComp node as string"""
413        return "[{} {}]".format(
414            node.elt.accept(self), " ".join(n.accept(self) for n in node.generators)
415        )
416
417    def visit_module(self, node):
418        """return an astroid.Module node as string"""
419        docs = f'"""{node.doc}"""\n\n' if node.doc else ""
420        return docs + "\n".join(n.accept(self) for n in node.body) + "\n\n"
421
422    def visit_name(self, node):
423        """return an astroid.Name node as string"""
424        return node.name
425
426    def visit_namedexpr(self, node):
427        """Return an assignment expression node as string"""
428        target = node.target.accept(self)
429        value = node.value.accept(self)
430        return f"{target} := {value}"
431
432    def visit_nonlocal(self, node):
433        """return an astroid.Nonlocal node as string"""
434        return f"nonlocal {', '.join(node.names)}"
435
436    def visit_pass(self, node):
437        """return an astroid.Pass node as string"""
438        return "pass"
439
440    def visit_raise(self, node):
441        """return an astroid.Raise node as string"""
442        if node.exc:
443            if node.cause:
444                return f"raise {node.exc.accept(self)} from {node.cause.accept(self)}"
445            return f"raise {node.exc.accept(self)}"
446        return "raise"
447
448    def visit_return(self, node):
449        """return an astroid.Return node as string"""
450        if node.is_tuple_return() and len(node.value.elts) > 1:
451            elts = [child.accept(self) for child in node.value.elts]
452            return f"return {', '.join(elts)}"
453
454        if node.value:
455            return f"return {node.value.accept(self)}"
456
457        return "return"
458
459    def visit_set(self, node):
460        """return an astroid.Set node as string"""
461        return "{%s}" % ", ".join(child.accept(self) for child in node.elts)
462
463    def visit_setcomp(self, node):
464        """return an astroid.SetComp node as string"""
465        return "{{{} {}}}".format(
466            node.elt.accept(self), " ".join(n.accept(self) for n in node.generators)
467        )
468
469    def visit_slice(self, node):
470        """return an astroid.Slice node as string"""
471        lower = node.lower.accept(self) if node.lower else ""
472        upper = node.upper.accept(self) if node.upper else ""
473        step = node.step.accept(self) if node.step else ""
474        if step:
475            return f"{lower}:{upper}:{step}"
476        return f"{lower}:{upper}"
477
478    def visit_subscript(self, node):
479        """return an astroid.Subscript node as string"""
480        idx = node.slice
481        if idx.__class__.__name__.lower() == "index":
482            idx = idx.value
483        idxstr = idx.accept(self)
484        if idx.__class__.__name__.lower() == "tuple" and idx.elts:
485            # Remove parenthesis in tuple and extended slice.
486            # a[(::1, 1:)] is not valid syntax.
487            idxstr = idxstr[1:-1]
488        return f"{self._precedence_parens(node, node.value)}[{idxstr}]"
489
490    def visit_tryexcept(self, node):
491        """return an astroid.TryExcept node as string"""
492        trys = [f"try:\n{self._stmt_list(node.body)}"]
493        for handler in node.handlers:
494            trys.append(handler.accept(self))
495        if node.orelse:
496            trys.append(f"else:\n{self._stmt_list(node.orelse)}")
497        return "\n".join(trys)
498
499    def visit_tryfinally(self, node):
500        """return an astroid.TryFinally node as string"""
501        return "try:\n{}\nfinally:\n{}".format(
502            self._stmt_list(node.body), self._stmt_list(node.finalbody)
503        )
504
505    def visit_tuple(self, node):
506        """return an astroid.Tuple node as string"""
507        if len(node.elts) == 1:
508            return f"({node.elts[0].accept(self)}, )"
509        return f"({', '.join(child.accept(self) for child in node.elts)})"
510
511    def visit_unaryop(self, node):
512        """return an astroid.UnaryOp node as string"""
513        if node.op == "not":
514            operator = "not "
515        else:
516            operator = node.op
517        return f"{operator}{self._precedence_parens(node, node.operand)}"
518
519    def visit_while(self, node):
520        """return an astroid.While node as string"""
521        whiles = f"while {node.test.accept(self)}:\n{self._stmt_list(node.body)}"
522        if node.orelse:
523            whiles = f"{whiles}\nelse:\n{self._stmt_list(node.orelse)}"
524        return whiles
525
526    def visit_with(self, node):  # 'with' without 'as' is possible
527        """return an astroid.With node as string"""
528        items = ", ".join(
529            f"{expr.accept(self)}" + (v and f" as {v.accept(self)}" or "")
530            for expr, v in node.items
531        )
532        return f"with {items}:\n{self._stmt_list(node.body)}"
533
534    def visit_yield(self, node):
535        """yield an ast.Yield node as string"""
536        yi_val = (" " + node.value.accept(self)) if node.value else ""
537        expr = "yield" + yi_val
538        if node.parent.is_statement:
539            return expr
540
541        return f"({expr})"
542
543    def visit_yieldfrom(self, node):
544        """Return an astroid.YieldFrom node as string."""
545        yi_val = (" " + node.value.accept(self)) if node.value else ""
546        expr = "yield from" + yi_val
547        if node.parent.is_statement:
548            return expr
549
550        return f"({expr})"
551
552    def visit_starred(self, node):
553        """return Starred node as string"""
554        return "*" + node.value.accept(self)
555
556    def visit_match(self, node: "Match") -> str:
557        """Return an astroid.Match node as string."""
558        return f"match {node.subject.accept(self)}:\n{self._stmt_list(node.cases)}"
559
560    def visit_matchcase(self, node: "MatchCase") -> str:
561        """Return an astroid.MatchCase node as string."""
562        guard_str = f" if {node.guard.accept(self)}" if node.guard else ""
563        return (
564            f"case {node.pattern.accept(self)}{guard_str}:\n"
565            f"{self._stmt_list(node.body)}"
566        )
567
568    def visit_matchvalue(self, node: "MatchValue") -> str:
569        """Return an astroid.MatchValue node as string."""
570        return node.value.accept(self)
571
572    @staticmethod
573    def visit_matchsingleton(node: "MatchSingleton") -> str:
574        """Return an astroid.MatchSingleton node as string."""
575        return str(node.value)
576
577    def visit_matchsequence(self, node: "MatchSequence") -> str:
578        """Return an astroid.MatchSequence node as string."""
579        if node.patterns is None:
580            return "[]"
581        return f"[{', '.join(p.accept(self) for p in node.patterns)}]"
582
583    def visit_matchmapping(self, node: "MatchMapping") -> str:
584        """Return an astroid.MatchMapping node as string."""
585        mapping_strings: List[str] = []
586        if node.keys and node.patterns:
587            mapping_strings.extend(
588                f"{key.accept(self)}: {p.accept(self)}"
589                for key, p in zip(node.keys, node.patterns)
590            )
591        if node.rest:
592            mapping_strings.append(f"**{node.rest.accept(self)}")
593        return f"{'{'}{', '.join(mapping_strings)}{'}'}"
594
595    def visit_matchclass(self, node: "MatchClass") -> str:
596        """Return an astroid.MatchClass node as string."""
597        if node.cls is None:
598            raise Exception(f"{node} does not have a 'cls' node")
599        class_strings: List[str] = []
600        if node.patterns:
601            class_strings.extend(p.accept(self) for p in node.patterns)
602        if node.kwd_attrs and node.kwd_patterns:
603            for attr, pattern in zip(node.kwd_attrs, node.kwd_patterns):
604                class_strings.append(f"{attr}={pattern.accept(self)}")
605        return f"{node.cls.accept(self)}({', '.join(class_strings)})"
606
607    def visit_matchstar(self, node: "MatchStar") -> str:
608        """Return an astroid.MatchStar node as string."""
609        return f"*{node.name.accept(self) if node.name else '_'}"
610
611    def visit_matchas(self, node: "MatchAs") -> str:
612        """Return an astroid.MatchAs node as string."""
613        # pylint: disable=import-outside-toplevel
614        # Prevent circular dependency
615        from astroid.nodes.node_classes import MatchClass, MatchMapping, MatchSequence
616
617        if isinstance(node.parent, (MatchSequence, MatchMapping, MatchClass)):
618            return node.name.accept(self) if node.name else "_"
619        return (
620            f"{node.pattern.accept(self) if node.pattern else '_'}"
621            f"{f' as {node.name.accept(self)}' if node.name else ''}"
622        )
623
624    def visit_matchor(self, node: "MatchOr") -> str:
625        """Return an astroid.MatchOr node as string."""
626        if node.patterns is None:
627            raise Exception(f"{node} does not have pattern nodes")
628        return " | ".join(p.accept(self) for p in node.patterns)
629
630    # These aren't for real AST nodes, but for inference objects.
631
632    def visit_frozenset(self, node):
633        return node.parent.accept(self)
634
635    def visit_super(self, node):
636        return node.parent.accept(self)
637
638    def visit_uninferable(self, node):
639        return str(node)
640
641    def visit_property(self, node):
642        return node.function.accept(self)
643
644    def visit_evaluatedobject(self, node):
645        return node.original.accept(self)
646
647    def visit_unknown(self, node: "Unknown") -> str:
648        return str(node)
649
650
651def _import_string(names):
652    """return a list of (name, asname) formatted as a string"""
653    _names = []
654    for name, asname in names:
655        if asname is not None:
656            _names.append(f"{name} as {asname}")
657        else:
658            _names.append(name)
659    return ", ".join(_names)
660
661
662# This sets the default indent to 4 spaces.
663to_code = AsStringVisitor("    ")
664