xref: /netbsd-src/external/mpl/bind/dist/doc/misc/checkgrammar.py (revision 8aaca124c0ad52af9550477f296b63debc7b4c98)
1*8aaca124Schristos############################################################################
2*8aaca124Schristos# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
3*8aaca124Schristos#
4*8aaca124Schristos# SPDX-License-Identifier: MPL-2.0
5*8aaca124Schristos#
6*8aaca124Schristos# This Source Code Form is subject to the terms of the Mozilla Public
7*8aaca124Schristos# License, v. 2.0. If a copy of the MPL was not distributed with this
8*8aaca124Schristos# file, you can obtain one at https://mozilla.org/MPL/2.0/.
9*8aaca124Schristos#
10*8aaca124Schristos# See the COPYRIGHT file distributed with this work for additional
11*8aaca124Schristos# information regarding copyright ownership.
12*8aaca124Schristos############################################################################
13*8aaca124Schristos
14*8aaca124Schristos"""
15*8aaca124SchristosUtility to check ISC config grammar consistency. It detects statement names
16*8aaca124Schristoswhich use different grammar depending on position in the configuration file.
17*8aaca124SchristosE.g. "max-zone-ttl" in dnssec-policy uses '<duration>'
18*8aaca124Schristosvs. '( unlimited | <duration> ) used in options.
19*8aaca124Schristos"""
20*8aaca124Schristos
21*8aaca124Schristosfrom collections import namedtuple
22*8aaca124Schristosfrom itertools import groupby
23*8aaca124Schristosimport fileinput
24*8aaca124Schristos
25*8aaca124Schristosimport parsegrammar
26*8aaca124Schristos
27*8aaca124Schristos
28*8aaca124Schristosdef statement2block(grammar, path):
29*8aaca124Schristos    """Return mapping statement name to "path" where it is allowed.
30*8aaca124Schristos    _top is placeholder name for the namesless topmost context.
31*8aaca124Schristos
32*8aaca124Schristos    E.g. {
33*8aaca124Schristos        'options: [('_top',)],
34*8aaca124Schristos        'server': [('_top', 'view'), ('_top',)],
35*8aaca124Schristos        'rate-limit': [('_top', 'options'), ('_top', 'view')],
36*8aaca124Schristos        'slip': [('_top', 'options', 'rate-limit'), ('_top', 'view', 'rate-limit')]
37*8aaca124Schristos    }
38*8aaca124Schristos    """
39*8aaca124Schristos    key2place = {}
40*8aaca124Schristos
41*8aaca124Schristos    for key in grammar:
42*8aaca124Schristos        assert not key.startswith("_")
43*8aaca124Schristos        key2place.setdefault(key, []).append(tuple(path))
44*8aaca124Schristos        if "_mapbody" in grammar[key]:
45*8aaca124Schristos            nested2block = statement2block(grammar[key]["_mapbody"], path + [key])
46*8aaca124Schristos            # merge to uppermost output dictionary
47*8aaca124Schristos            for nested_key, nested_path in nested2block.items():
48*8aaca124Schristos                key2place.setdefault(nested_key, []).extend(nested_path)
49*8aaca124Schristos    return key2place
50*8aaca124Schristos
51*8aaca124Schristos
52*8aaca124Schristosdef get_statement_grammar(grammar, path, name):
53*8aaca124Schristos    """Descend into grammar dict using provided path
54*8aaca124Schristos    and return final dict found there.
55*8aaca124Schristos
56*8aaca124Schristos    Intermediate steps into "_mapbody" subkeys are done automatically.
57*8aaca124Schristos    """
58*8aaca124Schristos    assert path[0] == "_top"
59*8aaca124Schristos    path = list(path) + [name]
60*8aaca124Schristos    for step in path[1:]:
61*8aaca124Schristos        if "_mapbody" in grammar:
62*8aaca124Schristos            grammar = grammar["_mapbody"]
63*8aaca124Schristos        grammar = grammar[step]
64*8aaca124Schristos    return grammar
65*8aaca124Schristos
66*8aaca124Schristos
67*8aaca124SchristosStatement = namedtuple("Statement", ["path", "name", "subgrammar"])
68*8aaca124Schristos
69*8aaca124Schristos
70*8aaca124Schristosdef groupby_grammar(statements):
71*8aaca124Schristos    """
72*8aaca124Schristos    Return groups of Statement tuples with identical grammars and flags.
73*8aaca124Schristos    See itertools.groupby.
74*8aaca124Schristos    """
75*8aaca124Schristos
76*8aaca124Schristos    def keyfunc(statement):
77*8aaca124Schristos        return sorted(statement.subgrammar.items())
78*8aaca124Schristos
79*8aaca124Schristos    groups = []
80*8aaca124Schristos    statements = sorted(statements, key=keyfunc)
81*8aaca124Schristos    for _key, group in groupby(statements, keyfunc):
82*8aaca124Schristos        groups.append(list(group))  # Store group iterator as a list
83*8aaca124Schristos    return groups
84*8aaca124Schristos
85*8aaca124Schristos
86*8aaca124Schristosdef diff_statements(whole_grammar, places):
87*8aaca124Schristos    """
88*8aaca124Schristos    Return map {statement name: [groups of [Statement]s with identical grammar].
89*8aaca124Schristos    """
90*8aaca124Schristos    out = {}
91*8aaca124Schristos    for statement_name, paths in places.items():
92*8aaca124Schristos        grammars = []
93*8aaca124Schristos        for path in paths:
94*8aaca124Schristos            statement_grammar = get_statement_grammar(
95*8aaca124Schristos                whole_grammar, path, statement_name
96*8aaca124Schristos            )
97*8aaca124Schristos            grammars.append(Statement(path, statement_name, statement_grammar))
98*8aaca124Schristos        groups = groupby_grammar(grammars)
99*8aaca124Schristos        out[statement_name] = groups
100*8aaca124Schristos    return out
101*8aaca124Schristos
102*8aaca124Schristos
103*8aaca124Schristosdef pformat_grammar(node, level=1):
104*8aaca124Schristos    """Pretty print a given grammar node in the same way as cfg_test would"""
105*8aaca124Schristos
106*8aaca124Schristos    def sortkey(item):
107*8aaca124Schristos        """Treat 'type' specially and always put it first, for zone types"""
108*8aaca124Schristos        key, _ = item
109*8aaca124Schristos        if key == "type":
110*8aaca124Schristos            return ""
111*8aaca124Schristos        return key
112*8aaca124Schristos
113*8aaca124Schristos    if "_grammar" in node:  # no nesting
114*8aaca124Schristos        assert "_id" not in node
115*8aaca124Schristos        assert "_mapbody" not in node
116*8aaca124Schristos        out = node["_grammar"] + ";"
117*8aaca124Schristos        if "_flags" in node:
118*8aaca124Schristos            out += " // " + ", ".join(node["_flags"])
119*8aaca124Schristos        return out + "\n"
120*8aaca124Schristos
121*8aaca124Schristos    # a nested map
122*8aaca124Schristos    out = ""
123*8aaca124Schristos    indent = level * "\t"
124*8aaca124Schristos    if not node.get("_ignore_this_level"):
125*8aaca124Schristos        if "_id" in node:
126*8aaca124Schristos            out += node["_id"] + " "
127*8aaca124Schristos        out += "{\n"
128*8aaca124Schristos
129*8aaca124Schristos    for key, subnode in sorted(node["_mapbody"].items(), key=sortkey):
130*8aaca124Schristos        if not subnode.get("_ignore_this_level"):
131*8aaca124Schristos            out += f"{indent}{subnode.get('_pprint_name', key)}"
132*8aaca124Schristos            inner_grammar = pformat_grammar(node["_mapbody"][key], level=level + 1)
133*8aaca124Schristos        else:  # act as if we were not in a map
134*8aaca124Schristos            inner_grammar = pformat_grammar(node["_mapbody"][key], level=level)
135*8aaca124Schristos        if inner_grammar[0] != ";":  # we _did_ find some arguments
136*8aaca124Schristos            out += " "
137*8aaca124Schristos        out += inner_grammar
138*8aaca124Schristos
139*8aaca124Schristos    if not node.get("_ignore_this_level"):
140*8aaca124Schristos        out += indent[:-1] + "};"  # unindent the closing bracket
141*8aaca124Schristos        if "_flags" in node:
142*8aaca124Schristos            out += " // " + ", ".join(node["_flags"])
143*8aaca124Schristos    return out + "\n"
144*8aaca124Schristos
145*8aaca124Schristos
146*8aaca124Schristosdef main():
147*8aaca124Schristos    """
148*8aaca124Schristos    Ingest output from cfg_test --grammar and print out statements which use
149*8aaca124Schristos    different grammar in different contexts.
150*8aaca124Schristos    """
151*8aaca124Schristos    with fileinput.input() as filein:
152*8aaca124Schristos        grammar = parsegrammar.parse_mapbody(filein)
153*8aaca124Schristos    places = statement2block(grammar, ["_top"])
154*8aaca124Schristos
155*8aaca124Schristos    for statementname, groups in diff_statements(grammar, places).items():
156*8aaca124Schristos        if len(groups) > 1:
157*8aaca124Schristos            print(f'statement "{statementname}" is inconsistent across blocks')
158*8aaca124Schristos            for group in groups:
159*8aaca124Schristos                print(
160*8aaca124Schristos                    "- path:", ", ".join(" -> ".join(variant.path) for variant in group)
161*8aaca124Schristos                )
162*8aaca124Schristos                print(" ", pformat_grammar(group[0].subgrammar, level=1))
163*8aaca124Schristos            print()
164*8aaca124Schristos
165*8aaca124Schristos
166*8aaca124Schristosif __name__ == "__main__":
167*8aaca124Schristos    main()
168