2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-22 18:19:42 +00:00
bind/doc/misc/checkgrammar.py
Petr Špaček 5c04e3c524
Sort grammar map keys while pretty printing them
It would be too easy if we could just call sorted(). Thanks to zone
grammar the most important key "type" gets sorted near end, so we pull
it up to the top using a hack.
2022-07-01 08:59:23 +02:00

168 lines
5.5 KiB
Python

############################################################################
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.
############################################################################
"""
Utility to check ISC config grammar consistency. It detects statement names
which use different grammar depending on position in the configuration file.
E.g. "max-zone-ttl" in dnssec-policy uses '<duration>'
vs. '( unlimited | <duration> ) used in options.
"""
from collections import namedtuple
from itertools import groupby
import fileinput
import parsegrammar
def statement2block(grammar, path):
"""Return mapping statement name to "path" where it is allowed.
_top is placeholder name for the namesless topmost context.
E.g. {
'options: [('_top',)],
'server': [('_top', 'view'), ('_top',)],
'rate-limit': [('_top', 'options'), ('_top', 'view')],
'slip': [('_top', 'options', 'rate-limit'), ('_top', 'view', 'rate-limit')]
}
"""
key2place = {}
for key in grammar:
assert not key.startswith("_")
key2place.setdefault(key, []).append(tuple(path))
if "_mapbody" in grammar[key]:
nested2block = statement2block(grammar[key]["_mapbody"], path + [key])
# merge to uppermost output dictionary
for nested_key, nested_path in nested2block.items():
key2place.setdefault(nested_key, []).extend(nested_path)
return key2place
def get_statement_grammar(grammar, path, name):
"""Descend into grammar dict using provided path
and return final dict found there.
Intermediate steps into "_mapbody" subkeys are done automatically.
"""
assert path[0] == "_top"
path = list(path) + [name]
for step in path[1:]:
if "_mapbody" in grammar:
grammar = grammar["_mapbody"]
grammar = grammar[step]
return grammar
Statement = namedtuple("Statement", ["path", "name", "subgrammar"])
def groupby_grammar(statements):
"""
Return groups of Statement tuples with identical grammars and flags.
See itertools.groupby.
"""
def keyfunc(statement):
return sorted(statement.subgrammar.items())
groups = []
statements = sorted(statements, key=keyfunc)
for _key, group in groupby(statements, keyfunc):
groups.append(list(group)) # Store group iterator as a list
return groups
def diff_statements(whole_grammar, places):
"""
Return map {statement name: [groups of [Statement]s with identical grammar].
"""
out = {}
for statement_name, paths in places.items():
grammars = []
for path in paths:
statement_grammar = get_statement_grammar(
whole_grammar, path, statement_name
)
grammars.append(Statement(path, statement_name, statement_grammar))
groups = groupby_grammar(grammars)
out[statement_name] = groups
return out
def pformat_grammar(node, level=1):
"""Pretty print a given grammar node in the same way as cfg_test would"""
def sortkey(item):
"""Treat 'type' specially and always put it first, for zone types"""
key, _ = item
if key == "type":
return ""
return key
if "_grammar" in node: # no nesting
assert "_id" not in node
assert "_mapbody" not in node
out = node["_grammar"] + ";"
if "_flags" in node:
out += " // " + ", ".join(node["_flags"])
return out + "\n"
# a nested map
out = ""
indent = level * "\t"
if not node.get("_ignore_this_level"):
if "_id" in node:
out += node["_id"] + " "
out += "{\n"
for key, subnode in sorted(node["_mapbody"].items(), key=sortkey):
if not subnode.get("_ignore_this_level"):
out += f"{indent}{subnode.get('_pprint_name', key)}"
inner_grammar = pformat_grammar(node["_mapbody"][key], level=level + 1)
else: # act as if we were not in a map
inner_grammar = pformat_grammar(node["_mapbody"][key], level=level)
if inner_grammar[0] != ";": # we _did_ find some arguments
out += " "
out += inner_grammar
if not node.get("_ignore_this_level"):
out += indent[:-1] + "};" # unindent the closing bracket
if "_flags" in node:
out += " // " + ", ".join(node["_flags"])
return out + "\n"
def main():
"""
Ingest output from cfg_test --grammar and print out statements which use
different grammar in different contexts.
"""
with fileinput.input() as filein:
grammar = parsegrammar.parse_mapbody(filein)
places = statement2block(grammar, ["_top"])
for statementname, groups in diff_statements(grammar, places).items():
if len(groups) > 1:
print(f'statement "{statementname}" is inconsistent across blocks')
for group in groups:
print(
"- path:", ", ".join(" -> ".join(variant.path) for variant in group)
)
print(" ", pformat_grammar(group[0].subgrammar, level=1))
print()
if __name__ == "__main__":
main()