Daniel Mensinger | d7ea066 | 2021-10-09 15:47:34 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
Dylan Baker | e991c4d | 2023-12-13 11:38:41 -0800 | [diff] [blame] | 3 | # SPDX-License-Identifier: Apache-2.0 |
Daniel Mensinger | d7ea066 | 2021-10-09 15:47:34 +0200 | [diff] [blame] | 4 | # Copyright 2021 The Meson development team |
| 5 | |
| 6 | import argparse |
| 7 | import json |
| 8 | from pathlib import Path |
| 9 | from copy import deepcopy |
| 10 | |
| 11 | import typing as T |
| 12 | |
| 13 | T_None = type(None) |
| 14 | |
| 15 | # Global root object |
| 16 | root: dict |
| 17 | |
| 18 | def assert_has_typed_keys(path: str, data: dict, keys: T.Dict[str, T.Any]) -> dict: |
| 19 | assert set(data.keys()).issuperset(keys.keys()), f'{path}: DIFF: {set(data.keys()).difference(keys.keys())}' |
| 20 | res = dict() |
| 21 | for key, val in keys.items(): |
| 22 | cur = data.pop(key) |
| 23 | assert isinstance(cur, val), f'{path}: type({key}: {cur}) != {val}' |
| 24 | res[key] = cur |
| 25 | return res |
| 26 | |
| 27 | def validate_base_obj(path: str, name: str, obj: dict) -> None: |
| 28 | expected: T.Dict[str, T.Any] = { |
| 29 | 'name': str, |
| 30 | 'description': str, |
| 31 | 'since': (str, T_None), |
| 32 | 'deprecated': (str, T_None), |
| 33 | 'notes': list, |
| 34 | 'warnings': list, |
| 35 | } |
| 36 | cur = assert_has_typed_keys(f'{path}.{name}', obj, expected) |
| 37 | assert cur['name'], f'{path}.{name}' |
| 38 | assert cur['description'], f'{path}.{name}' |
| 39 | assert cur['name'] == name, f'{path}.{name}' |
| 40 | assert all(isinstance(x, str) and x for x in cur['notes']), f'{path}.{name}' |
| 41 | assert all(isinstance(x, str) and x for x in cur['warnings']), f'{path}.{name}' |
| 42 | |
| 43 | def validate_type(path: str, typ: dict) -> None: |
| 44 | expected: T.Dict[str, T.Any] = { |
| 45 | 'obj': str, |
| 46 | 'holds': list, |
| 47 | } |
| 48 | cur = assert_has_typed_keys(path, typ, expected) |
| 49 | assert not typ, f'{path} has extra keys: {typ.keys()}' |
| 50 | assert cur['obj'] in root['objects'], path |
| 51 | for i in cur['holds']: |
| 52 | validate_type(path, i) |
| 53 | |
| 54 | def validate_arg(path: str, name: str, arg: dict) -> None: |
| 55 | validate_base_obj(path, name, arg) |
| 56 | expected: T.Dict[str, T.Any] = { |
| 57 | 'type': list, |
| 58 | 'type_str': str, |
| 59 | 'required': bool, |
| 60 | 'default': (str, T_None), |
| 61 | 'min_varargs': (int, T_None), |
| 62 | 'max_varargs': (int, T_None), |
| 63 | } |
| 64 | cur = assert_has_typed_keys(f'{path}.{name}', arg, expected) |
| 65 | assert not arg, f'{path}.{name} has extra keys: {arg.keys()}' |
| 66 | assert cur['type'], f'{path}.{name}' |
| 67 | assert cur['type_str'], f'{path}.{name}' |
| 68 | for i in cur['type']: |
| 69 | validate_type(f'{path}.{name}', i) |
| 70 | if cur['min_varargs'] is not None: |
| 71 | assert cur['min_varargs'] > 0, f'{path}.{name}' |
| 72 | if cur['max_varargs'] is not None: |
| 73 | assert cur['max_varargs'] > 0, f'{path}.{name}' |
| 74 | |
| 75 | def validate_function(path: str, name: str, func: dict) -> None: |
| 76 | validate_base_obj(path, name, func) |
| 77 | expected: T.Dict[str, T.Any] = { |
| 78 | 'returns': list, |
| 79 | 'returns_str': str, |
| 80 | 'example': (str, T_None), |
| 81 | 'posargs': dict, |
| 82 | 'optargs': dict, |
| 83 | 'kwargs': dict, |
| 84 | 'varargs': (dict, T_None), |
Daniel Mensinger | f192ecd | 2022-01-23 16:03:41 +0100 | [diff] [blame] | 85 | 'arg_flattening': bool, |
Daniel Mensinger | d7ea066 | 2021-10-09 15:47:34 +0200 | [diff] [blame] | 86 | } |
| 87 | cur = assert_has_typed_keys(f'{path}.{name}', func, expected) |
| 88 | assert not func, f'{path}.{name} has extra keys: {func.keys()}' |
| 89 | assert cur['returns'], f'{path}.{name}' |
| 90 | assert cur['returns_str'], f'{path}.{name}' |
| 91 | for i in cur['returns']: |
| 92 | validate_type(f'{path}.{name}', i) |
| 93 | for k, v in cur['posargs'].items(): |
| 94 | validate_arg(f'{path}.{name}', k, v) |
| 95 | for k, v in cur['optargs'].items(): |
| 96 | validate_arg(f'{path}.{name}', k, v) |
| 97 | for k, v in cur['kwargs'].items(): |
| 98 | validate_arg(f'{path}.{name}', k, v) |
| 99 | if cur['varargs']: |
| 100 | validate_arg(f'{path}.{name}', cur['varargs']['name'], cur['varargs']) |
| 101 | |
| 102 | def validate_object(path: str, name: str, obj: dict) -> None: |
| 103 | validate_base_obj(path, name, obj) |
| 104 | expected: T.Dict[str, T.Any] = { |
| 105 | 'example': (str, T_None), |
| 106 | 'object_type': str, |
| 107 | 'methods': dict, |
| 108 | 'is_container': bool, |
| 109 | 'extends': (str, T_None), |
| 110 | 'returned_by': list, |
| 111 | 'extended_by': list, |
| 112 | 'defined_by_module': (str, T_None), |
| 113 | } |
| 114 | cur = assert_has_typed_keys(f'{path}.{name}', obj, expected) |
| 115 | assert not obj, f'{path}.{name} has extra keys: {obj.keys()}' |
| 116 | for key, val in cur['methods'].items(): |
| 117 | validate_function(f'{path}.{name}', key, val) |
| 118 | if cur['extends'] is not None: |
| 119 | assert cur['extends'] in root['objects'], f'{path}.{name}' |
| 120 | assert all(isinstance(x, str) for x in cur['returned_by']), f'{path}.{name}' |
| 121 | assert all(isinstance(x, str) for x in cur['extended_by']), f'{path}.{name}' |
| 122 | assert all(x in root['objects'] for x in cur['extended_by']), f'{path}.{name}' |
| 123 | if cur['defined_by_module'] is not None: |
| 124 | assert cur['defined_by_module'] in root['objects'], f'{path}.{name}' |
| 125 | assert cur['object_type'] == 'RETURNED', f'{path}.{name}' |
| 126 | assert root['objects'][cur['defined_by_module']]['object_type'] == 'MODULE', f'{path}.{name}' |
| 127 | assert name in root['objects_by_type']['modules'][cur['defined_by_module']], f'{path}.{name}' |
| 128 | return |
| 129 | assert cur['object_type'] in {'ELEMENTARY', 'BUILTIN', 'MODULE', 'RETURNED'}, f'{path}.{name}' |
| 130 | if cur['object_type'] == 'ELEMENTARY': |
| 131 | assert name in root['objects_by_type']['elementary'], f'{path}.{name}' |
| 132 | if cur['object_type'] == 'BUILTIN': |
| 133 | assert name in root['objects_by_type']['builtins'], f'{path}.{name}' |
| 134 | if cur['object_type'] == 'RETURNED': |
| 135 | assert name in root['objects_by_type']['returned'], f'{path}.{name}' |
| 136 | if cur['object_type'] == 'MODULE': |
| 137 | assert name in root['objects_by_type']['modules'], f'{path}.{name}' |
| 138 | |
| 139 | def main() -> int: |
| 140 | global root |
| 141 | |
| 142 | parser = argparse.ArgumentParser(description='Meson JSON docs validator') |
| 143 | parser.add_argument('doc_file', type=Path, help='The JSON docs to validate') |
| 144 | args = parser.parse_args() |
| 145 | |
| 146 | root_tmp = json.loads(args.doc_file.read_text(encoding='utf-8')) |
| 147 | root = deepcopy(root_tmp) |
| 148 | assert isinstance(root, dict) |
| 149 | |
| 150 | expected: T.Dict[str, T.Any] = { |
| 151 | 'version_major': int, |
| 152 | 'version_minor': int, |
| 153 | 'meson_version': str, |
| 154 | 'functions': dict, |
| 155 | 'objects': dict, |
| 156 | 'objects_by_type': dict, |
| 157 | } |
| 158 | cur = assert_has_typed_keys('root', root_tmp, expected) |
| 159 | assert not root_tmp, f'root has extra keys: {root_tmp.keys()}' |
| 160 | |
| 161 | refs = cur['objects_by_type'] |
| 162 | expected = { |
| 163 | 'elementary': list, |
| 164 | 'builtins': list, |
| 165 | 'returned': list, |
| 166 | 'modules': dict, |
| 167 | } |
| 168 | assert_has_typed_keys(f'root.objects_by_type', refs, expected) |
| 169 | assert not refs, f'root.objects_by_type has extra keys: {refs.keys()}' |
| 170 | assert all(isinstance(x, str) for x in root['objects_by_type']['elementary']) |
| 171 | assert all(isinstance(x, str) for x in root['objects_by_type']['builtins']) |
| 172 | assert all(isinstance(x, str) for x in root['objects_by_type']['returned']) |
| 173 | assert all(isinstance(x, str) for x in root['objects_by_type']['modules']) |
| 174 | assert all(x in root['objects'] for x in root['objects_by_type']['elementary']) |
| 175 | assert all(x in root['objects'] for x in root['objects_by_type']['builtins']) |
| 176 | assert all(x in root['objects'] for x in root['objects_by_type']['returned']) |
| 177 | assert all(x in root['objects'] for x in root['objects_by_type']['modules']) |
| 178 | assert all(root['objects'][x]['object_type'] == 'ELEMENTARY' for x in root['objects_by_type']['elementary']) |
| 179 | assert all(root['objects'][x]['object_type'] == 'BUILTIN' for x in root['objects_by_type']['builtins']) |
| 180 | assert all(root['objects'][x]['object_type'] == 'RETURNED' for x in root['objects_by_type']['returned']) |
| 181 | assert all(root['objects'][x]['object_type'] == 'MODULE' for x in root['objects_by_type']['modules']) |
| 182 | |
| 183 | # Check that module references are correct |
| 184 | assert all(all(isinstance(x, str) for x in v) for k, v in root['objects_by_type']['modules'].items()) |
| 185 | assert all(all(x in root['objects'] for x in v) for k, v in root['objects_by_type']['modules'].items()) |
| 186 | assert all(all(root['objects'][x]['defined_by_module'] == k for x in v) for k, v in root['objects_by_type']['modules'].items()) |
| 187 | |
| 188 | for key, val in cur['functions'].items(): |
| 189 | validate_function('root', key, val) |
| 190 | for key, val in cur['objects'].items(): |
| 191 | validate_object('root', key, val) |
| 192 | |
| 193 | return 0 |
| 194 | |
| 195 | if __name__ == '__main__': |
| 196 | raise SystemExit(main()) |