1# mako/parsetree.py
2# Copyright 2006-2023 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7"""defines the parse tree components for Mako templates."""
8
9import re
10
11from mako import ast
12from mako import exceptions
13from mako import filters
14from mako import util
15
16
17class Node:
18
19    """base class for a Node in the parse tree."""
20
21    def __init__(self, source, lineno, pos, filename):
22        self.source = source
23        self.lineno = lineno
24        self.pos = pos
25        self.filename = filename
26
27    @property
28    def exception_kwargs(self):
29        return {
30            "source": self.source,
31            "lineno": self.lineno,
32            "pos": self.pos,
33            "filename": self.filename,
34        }
35
36    def get_children(self):
37        return []
38
39    def accept_visitor(self, visitor):
40        def traverse(node):
41            for n in node.get_children():
42                n.accept_visitor(visitor)
43
44        method = getattr(visitor, "visit" + self.__class__.__name__, traverse)
45        method(self)
46
47
48class TemplateNode(Node):
49
50    """a 'container' node that stores the overall collection of nodes."""
51
52    def __init__(self, filename):
53        super().__init__("", 0, 0, filename)
54        self.nodes = []
55        self.page_attributes = {}
56
57    def get_children(self):
58        return self.nodes
59
60    def __repr__(self):
61        return "TemplateNode(%s, %r)" % (
62            util.sorted_dict_repr(self.page_attributes),
63            self.nodes,
64        )
65
66
67class ControlLine(Node):
68
69    """defines a control line, a line-oriented python line or end tag.
70
71    e.g.::
72
73        % if foo:
74            (markup)
75        % endif
76
77    """
78
79    has_loop_context = False
80
81    def __init__(self, keyword, isend, text, **kwargs):
82        super().__init__(**kwargs)
83        self.text = text
84        self.keyword = keyword
85        self.isend = isend
86        self.is_primary = keyword in ["for", "if", "while", "try", "with"]
87        self.nodes = []
88        if self.isend:
89            self._declared_identifiers = []
90            self._undeclared_identifiers = []
91        else:
92            code = ast.PythonFragment(text, **self.exception_kwargs)
93            self._declared_identifiers = code.declared_identifiers
94            self._undeclared_identifiers = code.undeclared_identifiers
95
96    def get_children(self):
97        return self.nodes
98
99    def declared_identifiers(self):
100        return self._declared_identifiers
101
102    def undeclared_identifiers(self):
103        return self._undeclared_identifiers
104
105    def is_ternary(self, keyword):
106        """return true if the given keyword is a ternary keyword
107        for this ControlLine"""
108
109        cases = {
110            "if": {"else", "elif"},
111            "try": {"except", "finally"},
112            "for": {"else"},
113        }
114
115        return keyword in cases.get(self.keyword, set())
116
117    def __repr__(self):
118        return "ControlLine(%r, %r, %r, %r)" % (
119            self.keyword,
120            self.text,
121            self.isend,
122            (self.lineno, self.pos),
123        )
124
125
126class Text(Node):
127    """defines plain text in the template."""
128
129    def __init__(self, content, **kwargs):
130        super().__init__(**kwargs)
131        self.content = content
132
133    def __repr__(self):
134        return "Text(%r, %r)" % (self.content, (self.lineno, self.pos))
135
136
137class Code(Node):
138    """defines a Python code block, either inline or module level.
139
140    e.g.::
141
142        inline:
143        <%
144            x = 12
145        %>
146
147        module level:
148        <%!
149            import logger
150        %>
151
152    """
153
154    def __init__(self, text, ismodule, **kwargs):
155        super().__init__(**kwargs)
156        self.text = text
157        self.ismodule = ismodule
158        self.code = ast.PythonCode(text, **self.exception_kwargs)
159
160    def declared_identifiers(self):
161        return self.code.declared_identifiers
162
163    def undeclared_identifiers(self):
164        return self.code.undeclared_identifiers
165
166    def __repr__(self):
167        return "Code(%r, %r, %r)" % (
168            self.text,
169            self.ismodule,
170            (self.lineno, self.pos),
171        )
172
173
174class Comment(Node):
175    """defines a comment line.
176
177    # this is a comment
178
179    """
180
181    def __init__(self, text, **kwargs):
182        super().__init__(**kwargs)
183        self.text = text
184
185    def __repr__(self):
186        return "Comment(%r, %r)" % (self.text, (self.lineno, self.pos))
187
188
189class Expression(Node):
190    """defines an inline expression.
191
192    ${x+y}
193
194    """
195
196    def __init__(self, text, escapes, **kwargs):
197        super().__init__(**kwargs)
198        self.text = text
199        self.escapes = escapes
200        self.escapes_code = ast.ArgumentList(escapes, **self.exception_kwargs)
201        self.code = ast.PythonCode(text, **self.exception_kwargs)
202
203    def declared_identifiers(self):
204        return []
205
206    def undeclared_identifiers(self):
207        # TODO: make the "filter" shortcut list configurable at parse/gen time
208        return self.code.undeclared_identifiers.union(
209            self.escapes_code.undeclared_identifiers.difference(
210                filters.DEFAULT_ESCAPES
211            )
212        ).difference(self.code.declared_identifiers)
213
214    def __repr__(self):
215        return "Expression(%r, %r, %r)" % (
216            self.text,
217            self.escapes_code.args,
218            (self.lineno, self.pos),
219        )
220
221
222class _TagMeta(type):
223    """metaclass to allow Tag to produce a subclass according to
224    its keyword"""
225
226    _classmap = {}
227
228    def __init__(cls, clsname, bases, dict_):
229        if getattr(cls, "__keyword__", None) is not None:
230            cls._classmap[cls.__keyword__] = cls
231        super().__init__(clsname, bases, dict_)
232
233    def __call__(cls, keyword, attributes, **kwargs):
234        if ":" in keyword:
235            ns, defname = keyword.split(":")
236            return type.__call__(
237                CallNamespaceTag, ns, defname, attributes, **kwargs
238            )
239
240        try:
241            cls = _TagMeta._classmap[keyword]
242        except KeyError:
243            raise exceptions.CompileException(
244                "No such tag: '%s'" % keyword,
245                source=kwargs["source"],
246                lineno=kwargs["lineno"],
247                pos=kwargs["pos"],
248                filename=kwargs["filename"],
249            )
250        return type.__call__(cls, keyword, attributes, **kwargs)
251
252
253class Tag(Node, metaclass=_TagMeta):
254    """abstract base class for tags.
255
256    e.g.::
257
258        <%sometag/>
259
260        <%someothertag>
261            stuff
262        </%someothertag>
263
264    """
265
266    __keyword__ = None
267
268    def __init__(
269        self,
270        keyword,
271        attributes,
272        expressions,
273        nonexpressions,
274        required,
275        **kwargs,
276    ):
277        r"""construct a new Tag instance.
278
279        this constructor not called directly, and is only called
280        by subclasses.
281
282        :param keyword: the tag keyword
283
284        :param attributes: raw dictionary of attribute key/value pairs
285
286        :param expressions: a set of identifiers that are legal attributes,
287         which can also contain embedded expressions
288
289        :param nonexpressions: a set of identifiers that are legal
290         attributes, which cannot contain embedded expressions
291
292        :param \**kwargs:
293         other arguments passed to the Node superclass (lineno, pos)
294
295        """
296        super().__init__(**kwargs)
297        self.keyword = keyword
298        self.attributes = attributes
299        self._parse_attributes(expressions, nonexpressions)
300        missing = [r for r in required if r not in self.parsed_attributes]
301        if len(missing):
302            raise exceptions.CompileException(
303                (
304                    "Missing attribute(s): %s"
305                    % ",".join(repr(m) for m in missing)
306                ),
307                **self.exception_kwargs,
308            )
309
310        self.parent = None
311        self.nodes = []
312
313    def is_root(self):
314        return self.parent is None
315
316    def get_children(self):
317        return self.nodes
318
319    def _parse_attributes(self, expressions, nonexpressions):
320        undeclared_identifiers = set()
321        self.parsed_attributes = {}
322        for key in self.attributes:
323            if key in expressions:
324                expr = []
325                for x in re.compile(r"(\${.+?})", re.S).split(
326                    self.attributes[key]
327                ):
328                    m = re.compile(r"^\${(.+?)}$", re.S).match(x)
329                    if m:
330                        code = ast.PythonCode(
331                            m.group(1).rstrip(), **self.exception_kwargs
332                        )
333                        # we aren't discarding "declared_identifiers" here,
334                        # which we do so that list comprehension-declared
335                        # variables aren't counted.   As yet can't find a
336                        # condition that requires it here.
337                        undeclared_identifiers = undeclared_identifiers.union(
338                            code.undeclared_identifiers
339                        )
340                        expr.append("(%s)" % m.group(1))
341                    elif x:
342                        expr.append(repr(x))
343                self.parsed_attributes[key] = " + ".join(expr) or repr("")
344            elif key in nonexpressions:
345                if re.search(r"\${.+?}", self.attributes[key]):
346                    raise exceptions.CompileException(
347                        "Attribute '%s' in tag '%s' does not allow embedded "
348                        "expressions" % (key, self.keyword),
349                        **self.exception_kwargs,
350                    )
351                self.parsed_attributes[key] = repr(self.attributes[key])
352            else:
353                raise exceptions.CompileException(
354                    "Invalid attribute for tag '%s': '%s'"
355                    % (self.keyword, key),
356                    **self.exception_kwargs,
357                )
358        self.expression_undeclared_identifiers = undeclared_identifiers
359
360    def declared_identifiers(self):
361        return []
362
363    def undeclared_identifiers(self):
364        return self.expression_undeclared_identifiers
365
366    def __repr__(self):
367        return "%s(%r, %s, %r, %r)" % (
368            self.__class__.__name__,
369            self.keyword,
370            util.sorted_dict_repr(self.attributes),
371            (self.lineno, self.pos),
372            self.nodes,
373        )
374
375
376class IncludeTag(Tag):
377    __keyword__ = "include"
378
379    def __init__(self, keyword, attributes, **kwargs):
380        super().__init__(
381            keyword,
382            attributes,
383            ("file", "import", "args"),
384            (),
385            ("file",),
386            **kwargs,
387        )
388        self.page_args = ast.PythonCode(
389            "__DUMMY(%s)" % attributes.get("args", ""), **self.exception_kwargs
390        )
391
392    def declared_identifiers(self):
393        return []
394
395    def undeclared_identifiers(self):
396        identifiers = self.page_args.undeclared_identifiers.difference(
397            {"__DUMMY"}
398        ).difference(self.page_args.declared_identifiers)
399        return identifiers.union(super().undeclared_identifiers())
400
401
402class NamespaceTag(Tag):
403    __keyword__ = "namespace"
404
405    def __init__(self, keyword, attributes, **kwargs):
406        super().__init__(
407            keyword,
408            attributes,
409            ("file",),
410            ("name", "inheritable", "import", "module"),
411            (),
412            **kwargs,
413        )
414
415        self.name = attributes.get("name", "__anon_%s" % hex(abs(id(self))))
416        if "name" not in attributes and "import" not in attributes:
417            raise exceptions.CompileException(
418                "'name' and/or 'import' attributes are required "
419                "for <%namespace>",
420                **self.exception_kwargs,
421            )
422        if "file" in attributes and "module" in attributes:
423            raise exceptions.CompileException(
424                "<%namespace> may only have one of 'file' or 'module'",
425                **self.exception_kwargs,
426            )
427
428    def declared_identifiers(self):
429        return []
430
431
432class TextTag(Tag):
433    __keyword__ = "text"
434
435    def __init__(self, keyword, attributes, **kwargs):
436        super().__init__(keyword, attributes, (), ("filter"), (), **kwargs)
437        self.filter_args = ast.ArgumentList(
438            attributes.get("filter", ""), **self.exception_kwargs
439        )
440
441    def undeclared_identifiers(self):
442        return self.filter_args.undeclared_identifiers.difference(
443            filters.DEFAULT_ESCAPES.keys()
444        ).union(self.expression_undeclared_identifiers)
445
446
447class DefTag(Tag):
448    __keyword__ = "def"
449
450    def __init__(self, keyword, attributes, **kwargs):
451        expressions = ["buffered", "cached"] + [
452            c for c in attributes if c.startswith("cache_")
453        ]
454
455        super().__init__(
456            keyword,
457            attributes,
458            expressions,
459            ("name", "filter", "decorator"),
460            ("name",),
461            **kwargs,
462        )
463        name = attributes["name"]
464        if re.match(r"^[\w_]+$", name):
465            raise exceptions.CompileException(
466                "Missing parenthesis in %def", **self.exception_kwargs
467            )
468        self.function_decl = ast.FunctionDecl(
469            "def " + name + ":pass", **self.exception_kwargs
470        )
471        self.name = self.function_decl.funcname
472        self.decorator = attributes.get("decorator", "")
473        self.filter_args = ast.ArgumentList(
474            attributes.get("filter", ""), **self.exception_kwargs
475        )
476
477    is_anonymous = False
478    is_block = False
479
480    @property
481    def funcname(self):
482        return self.function_decl.funcname
483
484    def get_argument_expressions(self, **kw):
485        return self.function_decl.get_argument_expressions(**kw)
486
487    def declared_identifiers(self):
488        return self.function_decl.allargnames
489
490    def undeclared_identifiers(self):
491        res = []
492        for c in self.function_decl.defaults:
493            res += list(
494                ast.PythonCode(
495                    c, **self.exception_kwargs
496                ).undeclared_identifiers
497            )
498        return (
499            set(res)
500            .union(
501                self.filter_args.undeclared_identifiers.difference(
502                    filters.DEFAULT_ESCAPES.keys()
503                )
504            )
505            .union(self.expression_undeclared_identifiers)
506            .difference(self.function_decl.allargnames)
507        )
508
509
510class BlockTag(Tag):
511    __keyword__ = "block"
512
513    def __init__(self, keyword, attributes, **kwargs):
514        expressions = ["buffered", "cached", "args"] + [
515            c for c in attributes if c.startswith("cache_")
516        ]
517
518        super().__init__(
519            keyword,
520            attributes,
521            expressions,
522            ("name", "filter", "decorator"),
523            (),
524            **kwargs,
525        )
526        name = attributes.get("name")
527        if name and not re.match(r"^[\w_]+$", name):
528            raise exceptions.CompileException(
529                "%block may not specify an argument signature",
530                **self.exception_kwargs,
531            )
532        if not name and attributes.get("args", None):
533            raise exceptions.CompileException(
534                "Only named %blocks may specify args", **self.exception_kwargs
535            )
536        self.body_decl = ast.FunctionArgs(
537            attributes.get("args", ""), **self.exception_kwargs
538        )
539
540        self.name = name
541        self.decorator = attributes.get("decorator", "")
542        self.filter_args = ast.ArgumentList(
543            attributes.get("filter", ""), **self.exception_kwargs
544        )
545
546    is_block = True
547
548    @property
549    def is_anonymous(self):
550        return self.name is None
551
552    @property
553    def funcname(self):
554        return self.name or "__M_anon_%d" % (self.lineno,)
555
556    def get_argument_expressions(self, **kw):
557        return self.body_decl.get_argument_expressions(**kw)
558
559    def declared_identifiers(self):
560        return self.body_decl.allargnames
561
562    def undeclared_identifiers(self):
563        return (
564            self.filter_args.undeclared_identifiers.difference(
565                filters.DEFAULT_ESCAPES.keys()
566            )
567        ).union(self.expression_undeclared_identifiers)
568
569
570class CallTag(Tag):
571    __keyword__ = "call"
572
573    def __init__(self, keyword, attributes, **kwargs):
574        super().__init__(
575            keyword, attributes, ("args"), ("expr",), ("expr",), **kwargs
576        )
577        self.expression = attributes["expr"]
578        self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
579        self.body_decl = ast.FunctionArgs(
580            attributes.get("args", ""), **self.exception_kwargs
581        )
582
583    def declared_identifiers(self):
584        return self.code.declared_identifiers.union(self.body_decl.allargnames)
585
586    def undeclared_identifiers(self):
587        return self.code.undeclared_identifiers.difference(
588            self.code.declared_identifiers
589        )
590
591
592class CallNamespaceTag(Tag):
593    def __init__(self, namespace, defname, attributes, **kwargs):
594        super().__init__(
595            namespace + ":" + defname,
596            attributes,
597            tuple(attributes.keys()) + ("args",),
598            (),
599            (),
600            **kwargs,
601        )
602
603        self.expression = "%s.%s(%s)" % (
604            namespace,
605            defname,
606            ",".join(
607                "%s=%s" % (k, v)
608                for k, v in self.parsed_attributes.items()
609                if k != "args"
610            ),
611        )
612
613        self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
614        self.body_decl = ast.FunctionArgs(
615            attributes.get("args", ""), **self.exception_kwargs
616        )
617
618    def declared_identifiers(self):
619        return self.code.declared_identifiers.union(self.body_decl.allargnames)
620
621    def undeclared_identifiers(self):
622        return self.code.undeclared_identifiers.difference(
623            self.code.declared_identifiers
624        )
625
626
627class InheritTag(Tag):
628    __keyword__ = "inherit"
629
630    def __init__(self, keyword, attributes, **kwargs):
631        super().__init__(
632            keyword, attributes, ("file",), (), ("file",), **kwargs
633        )
634
635
636class PageTag(Tag):
637    __keyword__ = "page"
638
639    def __init__(self, keyword, attributes, **kwargs):
640        expressions = [
641            "cached",
642            "args",
643            "expression_filter",
644            "enable_loop",
645        ] + [c for c in attributes if c.startswith("cache_")]
646
647        super().__init__(keyword, attributes, expressions, (), (), **kwargs)
648        self.body_decl = ast.FunctionArgs(
649            attributes.get("args", ""), **self.exception_kwargs
650        )
651        self.filter_args = ast.ArgumentList(
652            attributes.get("expression_filter", ""), **self.exception_kwargs
653        )
654
655    def declared_identifiers(self):
656        return self.body_decl.allargnames
657