Fix dereference after NULL issue
[platform/upstream/libxkbcommon.git] / scripts / update-message-registry.py
1 #!/usr/bin/env python3
2
3 from __future__ import annotations
4
5 import argparse
6 from dataclasses import astuple, dataclass
7 from pathlib import Path
8 import re
9 from typing import Callable, Generic, Sequence, TypeVar
10
11 import jinja2
12 import yaml
13
14
15 @dataclass(order=True)
16 class Version:
17     """A semantic version number: MAJOR.MINOR.PATCH."""
18
19     UNKNOWN_VERSION = "ALWAYS"
20     DEFAULT_VERSION = "1.0.0"
21
22     major: int
23     minor: int
24     patch: int = 0
25
26     def __str__(self):
27         return ".".join(map(str, astuple(self)))
28
29     @classmethod
30     def parse(cls, raw_version: str) -> Version:
31         if raw_version == cls.UNKNOWN_VERSION:
32             raw_version = cls.DEFAULT_VERSION
33         version = raw_version.split(".")
34         assert 2 <= len(version) <= 3 and all(
35             n.isdecimal() for n in version
36         ), raw_version
37         return Version(*map(int, version))
38
39
40 @dataclass
41 class Example:
42     """An example in a message entry."""
43
44     name: str
45     description: str
46     before: str | None
47     after: str | None
48
49     @classmethod
50     def parse(cls, entry) -> Example:
51         name = entry.get("name")
52         assert name, entry
53
54         description = entry.get("description")
55         assert description
56
57         before = entry.get("before")
58         after = entry.get("after")
59         # Either none or both of them
60         assert not (bool(before) ^ bool(after))
61
62         return Example(name=name, description=description, before=before, after=after)
63
64
65 @dataclass
66 class Entry:
67     """An xkbcommon message entry in the message registry"""
68
69     VALID_TYPES = ("warning", "error")
70
71     code: int
72     """A unique strictly positive integer identifier"""
73     id: str
74     """A unique short human-readable string identifier"""
75     type: str
76     """Log level of the message"""
77     description: str
78     """A short description of the meaning of the message"""
79     details: str
80     """A long description of the meaning of the message"""
81     added: Version
82     """Version of xkbcommon the message has been added"""
83     removed: Version | None
84     """Version of xkbcommon the message has been removed"""
85     examples: tuple[Example, ...]
86     """
87     Optional examples of situations in which the message occurs.
88     If the message is an error or a warning, also provide hints on how to fix it.
89     """
90
91     @classmethod
92     def parse(cls, entry) -> Entry:
93         code = entry.get("code")
94         assert code is not None and isinstance(code, int) and code > 0, entry
95
96         id = entry.get("id")
97         assert id is not None, entry
98
99         type_ = entry.get("type")
100         assert type_ in cls.VALID_TYPES, entry
101
102         description = entry.get("description")
103         assert description is not None, entry
104
105         details = entry.get("details", "")
106
107         raw_added = entry.get("added", "")
108         assert raw_added, entry
109
110         added = Version.parse(raw_added)
111         assert added, entry
112
113         if removed := entry.get("removed"):
114             removed = Version.parse(removed)
115             assert added < removed, entry
116
117         if examples := entry.get("examples", ()):
118             examples = tuple(map(Example.parse, examples))
119
120         return Entry(
121             code=code,
122             id=id,
123             type=type_,
124             description=description,
125             added=added,
126             removed=removed,
127             details=details,
128             examples=examples,
129         )
130
131     @property
132     def message_code(self) -> str:
133         """Format the message code for display"""
134         return f"XKB-{self.code:0>3}"
135
136     @property
137     def message_code_constant(self: Entry) -> str:
138         """Returns the C enumeration member denoting the message code"""
139         id = self.id.replace("-", "_").upper()
140         return f"XKB_{self.type.upper()}_{id}"
141
142     @property
143     def message_name(self: Entry):
144         """Format the message string identifier for display"""
145         return self.id.replace("-", " ").capitalize()
146
147
148 def prepend_todo(text: str) -> str:
149     if text.startswith("TODO"):
150         return f"""<span class="todo">{text[:5]}</span>{text[5:]}"""
151     else:
152         return text
153
154
155 def load_message_registry(
156     env: jinja2.Environment, constants: dict[str, int], path: Path
157 ) -> Sequence[Entry]:
158     # Load the message registry YAML file as a Jinja2 template
159     registry_template = env.get_template(str(path))
160
161     # Load message registry
162     message_registry = sorted(
163         map(Entry.parse, yaml.safe_load(registry_template.render(constants))),
164         key=lambda e: e.code,
165     )
166
167     # Check message codes and identifiers are unique
168     codes: set[int] = set()
169     identifiers: set[str] = set()
170     for n, entry in enumerate(message_registry):
171         if entry.code in codes:
172             raise ValueError("Duplicated code in entry #{n}: {entry.code}")
173         if entry.id in identifiers:
174             raise ValueError("Duplicated identifier in entry #{n}: {entry.id}")
175         codes.add(entry.code)
176         identifiers.add(entry.id)
177
178     return message_registry
179
180
181 def generate(
182     registry: Sequence[Entry],
183     env: jinja2.Environment,
184     root: Path,
185     file: Path,
186     skip_removed: bool = False,
187 ):
188     """Generate a file from its Jinja2 template and the message registry"""
189     template_path = file.with_suffix(f"{file.suffix}.jinja")
190     template = env.get_template(str(template_path))
191     path = root / file
192     script = Path(__file__).name
193     with path.open("wt", encoding="utf-8") as fd:
194         entries = (
195             tuple(filter(lambda e: e.removed is None, registry))
196             if skip_removed
197             else registry
198         )
199         fd.writelines(template.generate(entries=entries, script=script))
200
201
202 T = TypeVar("T")
203
204
205 @dataclass
206 class Constant(Generic[T]):
207     name: str
208     pattern: re.Pattern
209     conversion: Callable[[str], T]
210
211
212 def read_constants(path: Path, patterns: Sequence[Constant[T]]) -> dict[str, T]:
213     constants: dict[str, T] = {}
214     patternsʹ = list(patterns)
215     with path.open("rt", encoding="utf-8") as fd:
216         for line in fd:
217             for k, constant in enumerate(patternsʹ):
218                 if m := constant.pattern.match(line):
219                     constants[constant.name] = constant.conversion(m.group(1))
220                     del patternsʹ[k]
221                     continue  # Expect only one match per line
222             if not patternsʹ:
223                 # No more pattern to match
224                 break
225     for constant in patternsʹ:
226         print(f"ERROR: could not find constant: {constant.name}.")
227     if patternsʹ:
228         raise ValueError("Some constants were not found.")
229     return constants
230
231
232 # Root of the project
233 ROOT = Path(__file__).parent.parent
234
235 # Parse commands
236 parser = argparse.ArgumentParser(description="Generate files from the message registry")
237 parser.add_argument(
238     "--root",
239     type=Path,
240     default=ROOT,
241     help="Path to the root of the project (default: %(default)s)",
242 )
243
244 args = parser.parse_args()
245
246 # Read some constants from libxkbcommon that we need
247 constants = read_constants(
248     Path(__file__).parent.parent / "src" / "keymap.h",
249     (Constant("XKB_MAX_GROUPS", re.compile("^#define\s+XKB_MAX_GROUPS\s+(\d+)"), int),),
250 )
251
252 # Configure Jinja
253 template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8")
254 jinja_env = jinja2.Environment(
255     loader=template_loader,
256     keep_trailing_newline=True,
257     trim_blocks=True,
258     lstrip_blocks=True,
259 )
260 jinja_env.filters["prepend_todo"] = prepend_todo
261
262 # Load message registry
263 message_registry = load_message_registry(
264     jinja_env, constants, Path("doc/message-registry.yaml")
265 )
266
267 # Generate the files
268 generate(
269     message_registry,
270     jinja_env,
271     args.root,
272     Path("src/messages-codes.h"),
273     skip_removed=True,
274 )
275 generate(
276     message_registry, jinja_env, args.root, Path("tools/messages.c"), skip_removed=True
277 )
278 generate(message_registry, jinja_env, args.root, Path("doc/message-registry.md"))