isar-tools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. isar_tools/__init__.py +1 -0
  2. isar_tools/__main__.py +5 -0
  3. isar_tools/checks/__init__.py +0 -0
  4. isar_tools/checks/cli.py +156 -0
  5. isar_tools/checks/docs.py +114 -0
  6. isar_tools/checks/findings.py +55 -0
  7. isar_tools/checks/locales.py +370 -0
  8. isar_tools/checks/project.py +97 -0
  9. isar_tools/checks/theory.py +117 -0
  10. isar_tools/cli.py +55 -0
  11. isar_tools/formatter/__init__.py +0 -0
  12. isar_tools/formatter/cli.py +103 -0
  13. isar_tools/formatter/formatter.py +377 -0
  14. isar_tools/formatter/wrap.py +103 -0
  15. isar_tools/project/__init__.py +0 -0
  16. isar_tools/project/cli.py +548 -0
  17. isar_tools/project/hierarchy.py +330 -0
  18. isar_tools/project/model.py +353 -0
  19. isar_tools/project/names.py +246 -0
  20. isar_tools/project/root.py +298 -0
  21. isar_tools/project/workspace.py +125 -0
  22. isar_tools/render.py +110 -0
  23. isar_tools/source/__init__.py +0 -0
  24. isar_tools/source/files.py +16 -0
  25. isar_tools/source/keywords.py +197 -0
  26. isar_tools/source/lexer.py +206 -0
  27. isar_tools/source/symbol_table.py +448 -0
  28. isar_tools/source/symbols.py +48 -0
  29. isar_tools/source/theory.py +387 -0
  30. isar_tools/stats/__init__.py +0 -0
  31. isar_tools/stats/build.py +278 -0
  32. isar_tools/stats/cli.py +213 -0
  33. isar_tools/stats/metrics.py +154 -0
  34. isar_tools/stats/views.py +295 -0
  35. isar_tools/style.py +73 -0
  36. isar_tools/symbols_cli.py +61 -0
  37. isar_tools-0.1.0.dist-info/METADATA +230 -0
  38. isar_tools-0.1.0.dist-info/RECORD +41 -0
  39. isar_tools-0.1.0.dist-info/WHEEL +4 -0
  40. isar_tools-0.1.0.dist-info/entry_points.txt +2 -0
  41. isar_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
isar_tools/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Source tooling for Isabelle/Isar projects."""
isar_tools/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from isar_tools.cli import main
4
+
5
+ sys.exit(main())
File without changes
@@ -0,0 +1,156 @@
1
+ """``isar check``: project and source hygiene."""
2
+
3
+ import argparse
4
+ import sys
5
+ from collections import Counter
6
+ from pathlib import Path
7
+
8
+ from isar_tools.checks.docs import check_docs
9
+ from isar_tools.checks.findings import CODES, DEFAULT_GROUPS, GROUPS, Finding
10
+ from isar_tools.checks.locales import check_locales
11
+ from isar_tools.checks.project import check_project
12
+ from isar_tools.checks.theory import check_proofs, check_symbols, check_syntax
13
+ from isar_tools.project.workspace import add_include_option, load
14
+ from isar_tools.render import RENDERERS, Column, Table, display_path
15
+ from isar_tools.style import Style, add_color_option
16
+
17
+ FORMATS = ("text", "json", "csv")
18
+
19
+
20
+ def register(sub: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: # pyright: ignore[reportPrivateUsage]
21
+ codes = "\n".join(f" {code:22} {group:8} {desc}" for code, (group, desc) in CODES.items())
22
+ check = sub.add_parser(
23
+ "check",
24
+ help="Check project and source hygiene",
25
+ description="Check project and source hygiene. Groups: "
26
+ f"{', '.join(GROUPS)}; default: {', '.join(DEFAULT_GROUPS)}.",
27
+ epilog=f"codes:\n{codes}",
28
+ formatter_class=argparse.RawDescriptionHelpFormatter,
29
+ )
30
+ check.add_argument(
31
+ "paths", nargs="*", type=Path, default=[Path()], help="project directories or .thy files"
32
+ )
33
+ check.add_argument(
34
+ "--group",
35
+ dest="groups",
36
+ action="append",
37
+ choices=GROUPS,
38
+ help="run this group of checks (repeatable; default: all but symbols, docs, and locales)",
39
+ )
40
+ check.add_argument(
41
+ "--ignore",
42
+ action="append",
43
+ default=[],
44
+ choices=sorted(CODES),
45
+ metavar="CODE",
46
+ help="do not report this code (repeatable)",
47
+ )
48
+ check.add_argument(
49
+ "--include-comments", action="store_true", help="symbols: also check (* *) comments"
50
+ )
51
+ check.add_argument(
52
+ "--allow",
53
+ action="append",
54
+ default=[],
55
+ metavar="NAME",
56
+ help="locales: do not report this identifier (repeatable)",
57
+ )
58
+ check.add_argument("--format", choices=FORMATS, default="text")
59
+ add_color_option(check)
60
+ add_include_option(check)
61
+ check.set_defaults(func=run)
62
+
63
+
64
+ def normalize_argv(argv: list[str]) -> list[str]:
65
+ """``check GROUP ...`` means ``check --group GROUP ...``."""
66
+ args = list(argv)
67
+ if len(args) > 1 and args[0] == "check" and args[1] in GROUPS:
68
+ args[1:2] = ["--group", args[1]]
69
+ return args
70
+
71
+
72
+ def collect_findings(args: argparse.Namespace) -> list[Finding]:
73
+ groups: set[str] = set(args.groups or DEFAULT_GROUPS)
74
+ workspace = load(args.paths, args.include)
75
+ findings: list[Finding] = []
76
+ if "project" in groups:
77
+ for project in workspace.projects:
78
+ findings += check_project(project)
79
+ if groups & {"proofs", "syntax", "symbols", "docs"}:
80
+ for source in workspace.sources:
81
+ theory = source.parse()
82
+ if "proofs" in groups:
83
+ findings += check_proofs(source.path, theory)
84
+ if "syntax" in groups:
85
+ findings += check_syntax(source.path, theory)
86
+ if "symbols" in groups:
87
+ findings += check_symbols(
88
+ source.path, theory, include_comments=args.include_comments
89
+ )
90
+ if "docs" in groups:
91
+ findings += check_docs(source.path, theory)
92
+ if "locales" in groups:
93
+ findings += check_locales(workspace.sources, allow=args.allow)
94
+ ignored = set(args.ignore)
95
+ return sorted({f for f in findings if f.code not in ignored})
96
+
97
+
98
+ def findings_table(findings: list[Finding]) -> Table:
99
+ return Table(
100
+ "findings",
101
+ "Findings",
102
+ [
103
+ Column("path", "path"),
104
+ Column("line", "line", True),
105
+ Column("column", "column", True),
106
+ Column("code", "code"),
107
+ Column("message", "message"),
108
+ ],
109
+ [
110
+ {
111
+ "path": display_path(f.path),
112
+ "line": f.line,
113
+ "column": f.column,
114
+ "code": f.code,
115
+ "message": f.message,
116
+ }
117
+ for f in findings
118
+ ],
119
+ )
120
+
121
+
122
+ # Colour of a finding's code, by group.
123
+ _GROUP_COLORS = {
124
+ "project": "magenta",
125
+ "proofs": "yellow",
126
+ "syntax": "red",
127
+ "symbols": "cyan",
128
+ "docs": "green",
129
+ "locales": "blue",
130
+ }
131
+
132
+
133
+ def summary(findings: list[Finding]) -> str:
134
+ """``2 findings: 1 missing-theory, 1 unfinished-proof``, most frequent first."""
135
+ if not findings:
136
+ return "no findings"
137
+ counts = Counter(f.code for f in findings)
138
+ parts = ", ".join(
139
+ f"{n} {code}" for code, n in sorted(counts.items(), key=lambda c: (-c[1], c[0]))
140
+ )
141
+ noun = "finding" if len(findings) == 1 else "findings"
142
+ return f"{len(findings)} {noun}: {parts}"
143
+
144
+
145
+ def run(args: argparse.Namespace) -> int:
146
+ findings = collect_findings(args)
147
+ if args.format == "text":
148
+ style = Style.for_stream(args.color, sys.stdout)
149
+ for f in findings:
150
+ where = style(display_path(f.path), "bold") + style(f":{f.line}:{f.column}:", "dim")
151
+ code = style(f.code, _GROUP_COLORS[CODES[f.code][0]], "bold")
152
+ print(f"{where} {code}: {f.message}")
153
+ print(summary(findings), file=sys.stderr)
154
+ else:
155
+ RENDERERS[args.format]([findings_table(findings)], sys.stdout)
156
+ return 1 if findings else 0
@@ -0,0 +1,114 @@
1
+ """Documentation coverage: the opt-in ``docs`` group.
2
+
3
+ A style policy, ported from Voblint's ``extract_definitions.py --lint``:
4
+ theories, headings, and locales and classes carry prose, routine
5
+ declarations need not. All rules are stated over the command sequence of
6
+ :class:`~isar_tools.source.theory.Theory`:
7
+
8
+ - A *text block* is a ``text`` (or ``txt``) command, with any argument form
9
+ and document tag. ``text_raw`` is raw LaTeX, not prose, and does not count.
10
+ Neither do ``(* *)`` comments, which are not part of the document, nor
11
+ formal comments ``\\<comment> \\<open>...\\<close>``, which annotate one
12
+ term or proof step and belong to the command they occur in.
13
+ - ``undocumented-theory``: no text block before the first command that is
14
+ none of: the ``theory`` header, a document command (heading, text,
15
+ ``text_raw``), or a preamble command that only opens a context or adjusts
16
+ syntax and name visibility (``context``, ``unbundle``, ``declare``,
17
+ ``hide_const``, ``notation``, ...; see ``_PREAMBLE``). Text before the
18
+ header counts.
19
+ - ``undocumented-heading``: a heading (``chapter`` ... ``subparagraph``)
20
+ whose next command is not a text block and whose previous command is not a
21
+ text block either. For a heading right before the ``theory`` header, the
22
+ next command is the first one after the header.
23
+ - ``undocumented-locale`` / ``undocumented-class``: a ``locale`` or ``class``
24
+ declaration whose previous command is not a text block.
25
+
26
+ "Next" and "previous" mean adjacent in the command sequence: blank lines and
27
+ comments in between do not matter, any other command does.
28
+ """
29
+
30
+ from itertools import dropwhile
31
+ from pathlib import Path
32
+
33
+ from isar_tools.checks.findings import Finding
34
+ from isar_tools.source.keywords import DOCUMENT, CommandKind
35
+ from isar_tools.source.theory import Command, Theory, significant, unquote
36
+
37
+ # Commands that may precede a theory's opening text: they open a context or
38
+ # adjust syntax and name visibility, but declare nothing.
39
+ _PREAMBLE = frozenset(
40
+ {
41
+ "context",
42
+ "unbundle",
43
+ "declare",
44
+ "hide_class",
45
+ "hide_type",
46
+ "hide_const",
47
+ "hide_fact",
48
+ "notation",
49
+ "no_notation",
50
+ "type_notation",
51
+ "no_type_notation",
52
+ "no_syntax",
53
+ "no_translations",
54
+ }
55
+ )
56
+ _SKIPPED = DOCUMENT | {CommandKind.THY_BEGIN}
57
+ # Declarations that must be preceded by a text block, with their finding code.
58
+ _DECLARATIONS = {"locale": "undocumented-locale", "class": "undocumented-class"}
59
+
60
+
61
+ def _is_text(command: Command | None) -> bool:
62
+ return command is not None and command.kind is CommandKind.DOCUMENT_BODY
63
+
64
+
65
+ def _argument(theory: Theory, command: Command) -> str:
66
+ """The first argument of ``command``, after modifiers (``private``) and
67
+ document tags, on one line."""
68
+ tokens = significant(command.tokens(theory.tokens))
69
+ args = list(dropwhile(lambda t: t.text != command.name, tokens))[1:]
70
+ while len(args) > 1 and args[0].text == "%":
71
+ args = args[2:]
72
+ return " ".join(unquote(args[0]).split()) if args else ""
73
+
74
+
75
+ def check_docs(path: Path, theory: Theory) -> list[Finding]:
76
+ commands = theory.commands
77
+ findings: list[Finding] = []
78
+
79
+ def report(command: Command, code: str, message: str) -> None:
80
+ findings.append(Finding.at(path, theory.lines, theory.start(command), code, message))
81
+
82
+ header = next((c for c in commands if c.kind is CommandKind.THY_BEGIN), None)
83
+ if header is not None:
84
+ opening = (
85
+ c for c in commands if _is_text(c) or not (c.kind in _SKIPPED or c.name in _PREAMBLE)
86
+ )
87
+ first = next(opening, None)
88
+ if not _is_text(first):
89
+ report(
90
+ header,
91
+ "undocumented-theory",
92
+ "theory has no text block before its first declaration",
93
+ )
94
+ for i, command in enumerate(commands):
95
+ previous = commands[i - 1] if i > 0 else None
96
+ if command.kind is CommandKind.DOCUMENT_HEADING:
97
+ j = i + 1
98
+ if j < len(commands) and commands[j].kind is CommandKind.THY_BEGIN:
99
+ j += 1
100
+ following = commands[j] if j < len(commands) else None
101
+ if not _is_text(following) and not _is_text(previous):
102
+ title = _argument(theory, command)
103
+ report(
104
+ command,
105
+ "undocumented-heading",
106
+ f"{command.name} {title!r} has no text block next to it",
107
+ )
108
+ elif command.name in _DECLARATIONS and not _is_text(previous):
109
+ report(
110
+ command,
111
+ _DECLARATIONS[command.name],
112
+ f"{command.name} {_argument(theory, command)} is not preceded by a text block",
113
+ )
114
+ return findings
@@ -0,0 +1,55 @@
1
+ """Findings of ``isar check`` and the registry of their codes."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from isar_tools.source.lexer import LineIndex
7
+
8
+
9
+ @dataclass(frozen=True, order=True)
10
+ class Finding:
11
+ path: Path
12
+ line: int # 1-based
13
+ column: int # 1-based
14
+ code: str
15
+ message: str
16
+
17
+ @classmethod
18
+ def at(cls, path: Path, lines: LineIndex, offset: int, code: str, message: str) -> "Finding":
19
+ return cls(path, lines.line(offset), lines.column(offset), code, message)
20
+
21
+
22
+ # code -> (group, description). Codes are stable identifiers for --ignore and
23
+ # machine-readable output.
24
+ CODES: dict[str, tuple[str, str]] = {
25
+ "root-syntax": ("project", "a ROOT file does not parse"),
26
+ "duplicate-session": ("project", "two sessions have the same name"),
27
+ "missing-theory": ("project", "a theories entry names no existing theory"),
28
+ "missing-directory": ("project", "a session or directories entry names no directory"),
29
+ "missing-document-file": ("project", "a document_files entry names no file"),
30
+ "duplicate-theory-name": (
31
+ "project",
32
+ "two theory files with the same name on one session's search path",
33
+ ),
34
+ "unreached-theory": (
35
+ "project",
36
+ "a theory file on a session's search path that no session builds",
37
+ ),
38
+ "unfinished-proof": ("proofs", "sorry or \\<proof> leaves a goal unproved"),
39
+ "oops": ("proofs", "oops abandons a goal"),
40
+ "unclosed-proof": ("proofs", "a proof does not end before the next theory command"),
41
+ "lexical-error": ("syntax", "an unterminated comment, string, cartouche, or verbatim"),
42
+ "document-argument": ("syntax", "a document command without exactly one text argument"),
43
+ "non-ascii": ("symbols", "a non-ASCII character outside (* *) comments"),
44
+ "undocumented-theory": ("docs", "no text block before a theory's first declaration"),
45
+ "undocumented-heading": ("docs", "a heading with no text block right after or before it"),
46
+ "undocumented-locale": ("docs", "a locale with no text block right before it"),
47
+ "undocumented-class": ("docs", "a type class with no text block right before it"),
48
+ "locale-free-variable": (
49
+ "locales",
50
+ "a locale or context header term names something defined nowhere (heuristic)",
51
+ ),
52
+ }
53
+
54
+ GROUPS = ("project", "proofs", "syntax", "symbols", "docs", "locales")
55
+ DEFAULT_GROUPS = ("project", "proofs", "syntax")