git.schokokeks.org
Repositories
Help
Report an Issue
derivepassphrase.git
Code
Commits
Branches
Tags
Suche
Strukturansicht:
90f7b44
Branches
Tags
documentation-tree
master
unstable/modularize-and-refactor-test-machinery
unstable/ssh-agent-socket-providers
wishlist
0.1.0
0.1.1
0.1.2
0.1.3
0.2.0
0.3.0
0.3.1
0.3.2
0.3.3
0.4.0
0.5.1
0.5.2
derivepassphrase.git
scripts
man_diagnostics.py
Ignore unknown marked messages in the manpage diagnostics
Marco Ricci
commited
90f7b44
at 2025-08-22 07:01:55
man_diagnostics.py
Blame
History
Raw
#!/usr/bin/python3 # SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> # # SPDX-License-Identifier: Zlib """Check for diagnostic messages not emitted in the manpages.""" from __future__ import annotations import pathlib import re import sys from typing import TYPE_CHECKING, Literal, NewType, cast sys.path.append(str(pathlib.Path(sys.argv[0]).resolve().parent.parent / "src")) from derivepassphrase._internals import cli_messages # noqa: PLC2701 if TYPE_CHECKING: from collections.abc import Iterator EnumName = NewType("EnumName", str) DiagnosticText = NewType("DiagnosticText", str) known_errors = cli_messages.ErrMsgTemplate.__members__ known_warnings = cli_messages.WarnMsgTemplate.__members__ mark_only = cast("DiagnosticText", "<mark only>") def _replace_known_metavars(string: str) -> str: return ( string.replace( "{service_metavar!s}", cli_messages.Label.VAULT_METAVAR_SERVICE.value.singular, ) .replace( "{service_metavar}", cli_messages.Label.VAULT_METAVAR_SERVICE.value.singular, ) .replace("{PROG_NAME!s}", cli_messages.PROG_NAME) .replace("{PROG_NAME}", cli_messages.PROG_NAME) .replace("{settings_type!s}", "global/service-specific settings") .replace("{settings_type}", "global/service-specific settings") ) # Use a double negative in the name ("does not mismatch text") because # this is an error condition check, and if the enum name doesn't exist # (because the manpage is outdated), then there is no mismatch. This is # clearer (to me at least) than erroneously claiming that a missing text # matches the desired pattern. def _mismatches_text( pattern: re.Pattern[str], enum_name: EnumName, name_type: Literal["warning", "error"], ) -> bool: while "." in enum_name: enum_name = cast("EnumName", enum_name.partition(".")[2]) try: enum_value = ( known_errors[enum_name].value if name_type == "error" else known_warnings[enum_name].value ) except KeyError: # No text, so no mismatch. return False texts = {enum_value.singular, enum_value.plural} - {""} return not all(pattern.match(_replace_known_metavars(t)) for t in texts) def _entries_from_text( text: DiagnosticText, enum_names: set[EnumName], ) -> Iterator[ tuple[ Literal["warning", "error"], tuple[DiagnosticText, EnumName], ] ]: assert text not in manpage_documented_warnings assert text not in manpage_documented_errors pattern_parts = [ ".*" if part == "%s" else re.escape(part) for part in re.split(r"(%s)", text) ] pattern = re.compile("".join(pattern_parts)) for name in enum_names: _class_name, dot, enum_entry = name.partition(".") assert dot == ".", f"Invalid enum name {name!r}" assert "." not in enum_entry, f"Unsupported enum name {name!r}" if name.startswith("WarnMsgTemplate."): assert not _mismatches_text( pattern, enum_name=name, name_type="warning" ), ( f"Warning text for {name} doesn't match the manpage: " f"{text!r} -> {pattern.pattern!r}" ) yield ("warning", (text, cast("EnumName", enum_entry))) if name.startswith("ErrMsgTemplate."): assert not _mismatches_text( pattern, enum_name=name, name_type="error" ), ( f"Error text for {name} doesn't match the manpage: " f"{text!r} -> {pattern.pattern!r}" ) yield ("error", (text, cast("EnumName", enum_entry))) def _check_manpage( path: pathlib.Path, ) -> Iterator[ tuple[ Literal["warning", "error"], tuple[DiagnosticText, EnumName], ] ]: enum_names: set[EnumName] = set() for line in path.read_text(encoding="UTF-8").splitlines(keepends=False): if enum_names and line.startswith(".It "): # Some *roff escape sequences need to be undone. This is not an # exhaustive list; new entries will be added based on the actual # manpages as the need arises. text = cast( "DiagnosticText", line.removeprefix(".It ").replace('"', "").replace(r"\-", "-"), ) yield from _entries_from_text(text=text, enum_names=enum_names) enum_names.clear() elif line.startswith(r".\" Message-ID (mark only):"): yield from _entries_from_mark_only( cast("EnumName", line.split(None, 4)[4]) ) elif line.startswith(r".\" Message-ID:"): enum_names.add(cast("EnumName", line.split(None, 2)[2])) def _entries_from_mark_only( name: EnumName, ) -> Iterator[ tuple[ Literal["warning", "error"], tuple[DiagnosticText, EnumName], ] ]: text = mark_only _class_name, dot, enum_entry = name.partition(".") assert dot == ".", f"Invalid enum name {name!r}" assert "." not in enum_entry, f"Unsupported enum name {name!r}" if name.startswith("WarnMsgTemplate."): yield ("warning", (text, cast("EnumName", enum_entry))) if name.startswith("ErrMsgTemplate."): yield ("error", (text, cast("EnumName", enum_entry))) def _check_manpagedoc( path: pathlib.Path, ) -> Iterator[ tuple[ Literal["warning", "error"], tuple[DiagnosticText, EnumName], ] ]: enum_names: set[EnumName] = set() for line in path.read_text(encoding="UTF-8").splitlines(keepends=False): if enum_names and line.startswith(("??? failure ", "??? warning ")): text = cast("DiagnosticText", line.split(None, 2)[2]) for ch in ['"', "`"]: assert text.startswith(ch) assert text.endswith(ch) text = cast("DiagnosticText", text[1:-1]) yield from _entries_from_text(text=text, enum_names=enum_names) enum_names.clear() elif line.startswith("<!-- Message-ID (mark only):") and line.endswith( "-->" ): name = cast( "EnumName", line.removeprefix("<!-- Message-ID (mark only):") .removesuffix("-->") .strip(), ) yield from _entries_from_mark_only(name) elif line.startswith("<!-- Message-ID:") and line.endswith("-->"): name = cast( "EnumName", line.removeprefix("<!-- Message-ID:") .removesuffix("-->") .strip(), ) enum_names.add(name) base = pathlib.Path(sys.argv[0]).resolve().parent.parent manpage_documented_errors: dict[EnumName, DiagnosticText] = {} manpage_documented_warnings: dict[EnumName, DiagnosticText] = {} manpagedoc_documented_errors: dict[EnumName, DiagnosticText] = {} manpagedoc_documented_warnings: dict[EnumName, DiagnosticText] = {} marks: set[EnumName] = set() for set_name, globs, errors, warnings in [ ( "manpages", sorted( pathlib.Path(base, "share", "man", "man1").glob( "derivepassphrase*.1" ) ), manpage_documented_errors, manpage_documented_warnings, ), ( "manpage-ish docs", sorted( pathlib.Path(base, "docs", "reference").glob( "derivepassphrase*.1.md" ) ), manpagedoc_documented_errors, manpagedoc_documented_warnings, ), ]: for path in globs: print(f"CHECK DOC {str(path.relative_to(base))!r}") checker = ( _check_manpage if set_name == "manpages" else _check_manpagedoc ) for diagnostic_type, (text, name) in checker(path): if text == mark_only: marks.add(name) if diagnostic_type == "warning": warnings[name] = text print(f"DOC WARN {name} {text!r}") else: errors[name] = text print(f"DOC ERR {name} {text!r}") assert set(errors) >= set(known_errors), ( f"Some error messages aren't documented in the {set_name}: " + repr(set(known_errors) - set(errors)) ) assert set(warnings) >= set(known_warnings), ( f"Some warning messages aren't documented in the {set_name}: " + repr(set(known_warnings) - set(warnings)) ) assert set(errors) - marks <= set(known_errors), ( f"Some unknown error messages are documented in the {set_name}: " + repr(set(errors) - marks - set(known_errors)) # type: ignore[arg-type] ) assert set(warnings) - marks <= set(known_warnings), ( f"Some unknown warning messages are documented in the {set_name}: " + repr(set(warnings) - marks - set(known_warnings)) # type: ignore[arg-type] ) py_file_errors: set[EnumName] = set() py_file_warnings: set[EnumName] = set() match_errors_warnings = re.compile( r"\b(?:cli_messages|msg|_msg)\.(Err|Warn)MsgTemplate\.([A-Z0-9_]+)" ) for path in sorted( pathlib.Path(base, "src", "derivepassphrase").glob("**/*.py") ): if path != pathlib.Path( base, "src", "derivepassphrase", "_internals", "cli_messages.py" ): print(f"CHECK SOURCE {str(path.relative_to(base))!r}") filecontents = path.read_text(encoding="UTF-8") for match in match_errors_warnings.finditer(filecontents): message_type, symbol = match.group(1, 2) if message_type == "Err": py_file_errors.add(cast("EnumName", symbol)) print(f"SOURCE ERR {symbol}") elif message_type == "Warn": py_file_warnings.add(cast("EnumName", symbol)) print(f"SOURCE WARN {symbol}") if py_file_errors != set(known_errors): print( "Some error messages aren't in use: " + repr(set(known_errors) - py_file_errors), file=sys.stderr, ) if py_file_warnings != set(known_warnings): print( "Some warning messages aren't in use: " + repr(set(known_warnings) - py_file_warnings), file=sys.stderr, )