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.
- isar_tools/__init__.py +1 -0
- isar_tools/__main__.py +5 -0
- isar_tools/checks/__init__.py +0 -0
- isar_tools/checks/cli.py +156 -0
- isar_tools/checks/docs.py +114 -0
- isar_tools/checks/findings.py +55 -0
- isar_tools/checks/locales.py +370 -0
- isar_tools/checks/project.py +97 -0
- isar_tools/checks/theory.py +117 -0
- isar_tools/cli.py +55 -0
- isar_tools/formatter/__init__.py +0 -0
- isar_tools/formatter/cli.py +103 -0
- isar_tools/formatter/formatter.py +377 -0
- isar_tools/formatter/wrap.py +103 -0
- isar_tools/project/__init__.py +0 -0
- isar_tools/project/cli.py +548 -0
- isar_tools/project/hierarchy.py +330 -0
- isar_tools/project/model.py +353 -0
- isar_tools/project/names.py +246 -0
- isar_tools/project/root.py +298 -0
- isar_tools/project/workspace.py +125 -0
- isar_tools/render.py +110 -0
- isar_tools/source/__init__.py +0 -0
- isar_tools/source/files.py +16 -0
- isar_tools/source/keywords.py +197 -0
- isar_tools/source/lexer.py +206 -0
- isar_tools/source/symbol_table.py +448 -0
- isar_tools/source/symbols.py +48 -0
- isar_tools/source/theory.py +387 -0
- isar_tools/stats/__init__.py +0 -0
- isar_tools/stats/build.py +278 -0
- isar_tools/stats/cli.py +213 -0
- isar_tools/stats/metrics.py +154 -0
- isar_tools/stats/views.py +295 -0
- isar_tools/style.py +73 -0
- isar_tools/symbols_cli.py +61 -0
- isar_tools-0.1.0.dist-info/METADATA +230 -0
- isar_tools-0.1.0.dist-info/RECORD +41 -0
- isar_tools-0.1.0.dist-info/WHEEL +4 -0
- isar_tools-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
File without changes
|
isar_tools/checks/cli.py
ADDED
|
@@ -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")
|