normik 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 (102) hide show
  1. nk/__init__.py +3 -0
  2. nk/cli.py +331 -0
  3. nk/core/__init__.py +1 -0
  4. nk/core/baseline.py +94 -0
  5. nk/core/diagnostics.py +44 -0
  6. nk/core/document.py +219 -0
  7. nk/core/finding.py +97 -0
  8. nk/core/fixer.py +135 -0
  9. nk/core/numbering.py +82 -0
  10. nk/core/position.py +50 -0
  11. nk/core/profile.py +197 -0
  12. nk/core/registry.py +91 -0
  13. nk/core/rule.py +225 -0
  14. nk/core/runner.py +197 -0
  15. nk/core/suppressions.py +51 -0
  16. nk/parse/__init__.py +1 -0
  17. nk/parse/issues.py +60 -0
  18. nk/parse/numbering.py +194 -0
  19. nk/parse/structure.py +336 -0
  20. nk/parse/suppressions.py +48 -0
  21. nk/parse/tex.py +236 -0
  22. nk/profiles/__init__.py +1 -0
  23. nk/profiles/base.toml +17 -0
  24. nk/py.typed +0 -0
  25. nk/report/__init__.py +1 -0
  26. nk/report/agent.py +78 -0
  27. nk/report/common.py +57 -0
  28. nk/report/examples.py +69 -0
  29. nk/report/human.py +81 -0
  30. nk/report/json.py +55 -0
  31. nk/report/rules_docs.py +213 -0
  32. nk/rules/__init__.py +14 -0
  33. nk/rules/_shared.py +324 -0
  34. nk/rules/_text.py +46 -0
  35. nk/rules/elements/__init__.py +1 -0
  36. nk/rules/elements/appendix_figure_numbering.py +46 -0
  37. nk/rules/elements/appendix_formula_numbering.py +46 -0
  38. nk/rules/elements/appendix_table_numbering.py +46 -0
  39. nk/rules/elements/bibliography_bibtex.py +48 -0
  40. nk/rules/elements/bibliography_cite_unresolved.py +66 -0
  41. nk/rules/elements/bibliography_order.py +69 -0
  42. nk/rules/elements/bibliography_uncited.py +47 -0
  43. nk/rules/elements/figure_caption_capital.py +50 -0
  44. nk/rules/elements/figure_caption_dot.py +44 -0
  45. nk/rules/elements/figure_caption_hyphenation.py +50 -0
  46. nk/rules/elements/figure_caption_manual_number.py +55 -0
  47. nk/rules/elements/figure_caption_position.py +45 -0
  48. nk/rules/elements/figure_reference.py +54 -0
  49. nk/rules/elements/figure_reference_word.py +44 -0
  50. nk/rules/elements/footnote_space.py +53 -0
  51. nk/rules/elements/formula_blank_lines.py +62 -0
  52. nk/rules/elements/formula_reference.py +51 -0
  53. nk/rules/elements/formula_reference_format.py +59 -0
  54. nk/rules/elements/formula_where_colon.py +46 -0
  55. nk/rules/elements/note_capital.py +50 -0
  56. nk/rules/elements/note_dash.py +68 -0
  57. nk/rules/elements/numbering_scheme_mixed.py +48 -0
  58. nk/rules/elements/table_caption_capital.py +49 -0
  59. nk/rules/elements/table_caption_dot.py +42 -0
  60. nk/rules/elements/table_caption_manual_number.py +53 -0
  61. nk/rules/elements/table_caption_position.py +43 -0
  62. nk/rules/elements/table_reference.py +52 -0
  63. nk/rules/elements/table_reference_word.py +41 -0
  64. nk/rules/layout/__init__.py +1 -0
  65. nk/rules/layout/enumeration_letters.py +55 -0
  66. nk/rules/layout/heading_depth.py +49 -0
  67. nk/rules/layout/heading_dot.py +47 -0
  68. nk/rules/layout/heading_empty.py +47 -0
  69. nk/rules/layout/heading_hyphenation.py +51 -0
  70. nk/rules/layout/manual_section_number.py +71 -0
  71. nk/rules/layout/structural_heading_case.py +52 -0
  72. nk/rules/layout/structural_heading_numbered.py +49 -0
  73. nk/rules/structure/__init__.py +1 -0
  74. nk/rules/structure/abbreviations_opening.py +55 -0
  75. nk/rules/structure/abstract_volume_info.py +53 -0
  76. nk/rules/structure/abstract_volume_inline.py +91 -0
  77. nk/rules/structure/appendix_internal_numbering.py +54 -0
  78. nk/rules/structure/appendix_letter.py +52 -0
  79. nk/rules/structure/appendix_page_break.py +59 -0
  80. nk/rules/structure/appendix_reference.py +67 -0
  81. nk/rules/structure/appendix_sequence.py +66 -0
  82. nk/rules/structure/elements_order.py +56 -0
  83. nk/rules/structure/keywords_count.py +57 -0
  84. nk/rules/structure/keywords_final_dot.py +38 -0
  85. nk/rules/structure/keywords_uppercase.py +43 -0
  86. nk/rules/structure/required_elements.py +66 -0
  87. nk/rules/structure/terms_opening.py +54 -0
  88. nk/rules/typography/__init__.py +6 -0
  89. nk/rules/typography/abbreviations_dash.py +55 -0
  90. nk/rules/typography/initials_nbsp.py +81 -0
  91. nk/rules/typography/particle_nbsp.py +56 -0
  92. nk/rules/typography/preposition_nbsp.py +79 -0
  93. nk/rules/typography/reference_nbsp.py +53 -0
  94. nk/rules/typography/terms_dash.py +55 -0
  95. nk/rules/typography/text_dash.py +83 -0
  96. nk/rules/typography/text_quotes.py +64 -0
  97. nk/rules/typography/unit_nbsp.py +69 -0
  98. normik-0.1.0.dist-info/METADATA +140 -0
  99. normik-0.1.0.dist-info/RECORD +102 -0
  100. normik-0.1.0.dist-info/WHEEL +4 -0
  101. normik-0.1.0.dist-info/entry_points.txt +2 -0
  102. normik-0.1.0.dist-info/licenses/LICENSE +21 -0
nk/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """nk — линтер оформления отчёта о НИР по ГОСТ 7.32-2017 для исходников LaTeX."""
2
+
3
+ __version__ = "0.1.0"
nk/cli.py ADDED
@@ -0,0 +1,331 @@
1
+ """Команды CLI. Вся логика — в ядре; здесь только разбор аргументов и вывод."""
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from nk import __version__
13
+ from nk.core.baseline import Baseline, BaselineError
14
+ from nk.core.diagnostics import INTERNAL
15
+ from nk.core.finding import Severity
16
+ from nk.core.fixer import diff, plan, write
17
+ from nk.core.profile import Profile, ProfileError, load_profile
18
+ from nk.core.registry import load_rules, partition_ids, select_rules, validate_profile
19
+ from nk.core.rule import RuleImpl, UnknownRuleError
20
+ from nk.core.runner import RunResult, run
21
+ from nk.parse.tex import parse, parse_findings
22
+ from nk.report import agent, examples, human, rules_docs
23
+ from nk.report import json as json_report
24
+
25
+ app = typer.Typer(
26
+ name="nk",
27
+ help="Линтер оформления отчёта о НИР по ГОСТ 7.32-2017 для исходников LaTeX.",
28
+ no_args_is_help=True,
29
+ add_completion=False,
30
+ )
31
+ rules_app = typer.Typer(name="rules", help="Сведения о правилах.", no_args_is_help=True)
32
+ profile_app = typer.Typer(name="profile", help="Сведения о профилях.", no_args_is_help=True)
33
+ app.add_typer(rules_app)
34
+ app.add_typer(profile_app)
35
+
36
+ console = Console()
37
+ err_console = Console(stderr=True)
38
+
39
+ EXIT_OK = 0
40
+ EXIT_FOUND_ERRORS = 1
41
+ EXIT_INTERNAL_ERROR = 2
42
+
43
+ #: Сколько раз перепроверять исходники, применяя найденные правки.
44
+ FIX_PASSES = 5
45
+
46
+
47
+ class OutputFormat(StrEnum):
48
+ HUMAN = "human"
49
+ AGENT = "agent"
50
+ JSON = "json"
51
+
52
+
53
+ @app.callback(invoke_without_command=True)
54
+ def main(
55
+ version: bool = typer.Option(False, "--version", help="Показать версию и выйти."),
56
+ ) -> None:
57
+ if version:
58
+ console.print(f"nk {__version__}")
59
+ raise typer.Exit()
60
+
61
+
62
+ @rules_app.command("list")
63
+ def rules_list() -> None:
64
+ """Перечень правил: идентификатор, пункт стандарта, уровень, название."""
65
+ registry = load_rules()
66
+ if not len(registry):
67
+ console.print("Правил пока нет.")
68
+ return
69
+
70
+ table = Table(box=None, pad_edge=False)
71
+ table.add_column("ID")
72
+ table.add_column("Пункт")
73
+ table.add_column("Уровень")
74
+ table.add_column("Название")
75
+ for impl in registry:
76
+ table.add_row(impl.id, impl.clause, impl.severity.value, impl.title)
77
+ console.print(table)
78
+
79
+
80
+ @rules_app.command("show")
81
+ def rules_show(rule_id: str = typer.Argument(..., help="Идентификатор правила.")) -> None:
82
+ """Подробности по правилу."""
83
+ registry = load_rules()
84
+ try:
85
+ impl = registry.get(rule_id)
86
+ except UnknownRuleError:
87
+ err_console.print(f"Неизвестное правило: {rule_id}")
88
+ raise typer.Exit(EXIT_INTERNAL_ERROR) from None
89
+
90
+ console.print(f"[bold]{impl.id}[/bold] — {impl.title}")
91
+ console.print(f"Пункт ГОСТ 7.32-2017: {impl.clause}")
92
+ console.print(f"Уровень по умолчанию: {impl.severity.value}")
93
+ console.print(f"Объявлено в: {impl.module}")
94
+ if impl.description:
95
+ console.print("")
96
+ console.print(impl.description)
97
+ console.print("")
98
+ if impl.default_params:
99
+ console.print("Параметры по умолчанию:")
100
+ for name, value in sorted(impl.default_params.items()):
101
+ console.print(f" {name} = {value!r}")
102
+
103
+
104
+ @profile_app.command("show")
105
+ def profile_show(
106
+ profile_source: str = typer.Option(
107
+ None, "--profile", "-p", help="Имя встроенного профиля либо путь к TOML."
108
+ ),
109
+ ) -> None:
110
+ """Итоговый набор правил после применения профиля."""
111
+ registry = load_rules()
112
+ try:
113
+ profile = load_profile(profile_source)
114
+ validate_profile(profile, registry)
115
+ except ProfileError as error:
116
+ err_console.print(f"Профиль: {error}")
117
+ raise typer.Exit(EXIT_INTERNAL_ERROR) from None
118
+
119
+ profile = profile.resolve(registry.default_params())
120
+ active = {impl.id for impl in select_rules(registry, profile=profile)}
121
+
122
+ console.print(f"Профиль: [bold]{profile.name}[/bold]")
123
+ console.print(f"Правил включено: {len(active)} из {len(registry)}")
124
+ if not len(registry):
125
+ return
126
+
127
+ table = Table(box=None, pad_edge=False)
128
+ table.add_column("ID")
129
+ table.add_column("Уровень")
130
+ table.add_column("Состояние")
131
+ table.add_column("Параметры")
132
+ for impl in registry:
133
+ severity = profile.severity_for(impl.id, impl.severity)
134
+ changed = "" if severity is impl.severity else f" (было {impl.severity.value})"
135
+ params = profile.params_for(impl.id)
136
+ table.add_row(
137
+ impl.id,
138
+ f"{severity.value}{changed}",
139
+ "включено" if impl.id in active else "отключено",
140
+ ", ".join(f"{k}={v!r}" for k, v in sorted(params.items())),
141
+ )
142
+ console.print(table)
143
+
144
+
145
+ @app.command("check")
146
+ def check(
147
+ paths: list[Path] = typer.Argument(..., help="Файлы или каталоги с исходниками .tex."),
148
+ profile_source: str = typer.Option(
149
+ None, "--profile", "-p", help="Имя встроенного профиля либо путь к TOML."
150
+ ),
151
+ output_format: OutputFormat = typer.Option(
152
+ OutputFormat.HUMAN, "--format", "-f", help="Формат вывода."
153
+ ),
154
+ select: str = typer.Option(
155
+ None, "--select", help="Запустить только эти правила, через запятую."
156
+ ),
157
+ ignore: str = typer.Option(None, "--ignore", help="Исключить эти правила, через запятую."),
158
+ severity: Severity = typer.Option(
159
+ Severity.INFO, "--severity", help="Не показывать находки ниже уровня."
160
+ ),
161
+ limit: int = typer.Option(
162
+ agent.DEFAULT_LIMIT, "--limit", help="Предел числа находок в выводе; 0 — без предела."
163
+ ),
164
+ fix: bool = typer.Option(False, "--fix", help="Применить правки к исходникам."),
165
+ show_diff: bool = typer.Option(
166
+ False, "--diff", help="Показать правки как diff, ничего не записывая."
167
+ ),
168
+ baseline_path: Path = typer.Option(
169
+ None, "--baseline", help="Снимок известных нарушений: показывать только новые."
170
+ ),
171
+ write_baseline: Path = typer.Option(
172
+ None, "--write-baseline", help="Записать снимок текущих находок и выйти."
173
+ ),
174
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Только код возврата."),
175
+ ) -> None:
176
+ """Проверить исходники отчёта на соответствие ГОСТ 7.32-2017."""
177
+ missing = [path for path in paths if not path.exists()]
178
+ if missing:
179
+ err_console.print("Не найдено: " + ", ".join(str(path) for path in missing))
180
+ raise typer.Exit(EXIT_INTERNAL_ERROR)
181
+
182
+ registry = load_rules()
183
+ try:
184
+ profile = load_profile(profile_source)
185
+ validate_profile(profile, registry)
186
+ selected_rules, _ = partition_ids(_split(select))
187
+ ignored_rules, ignored_internal = partition_ids(_split(ignore))
188
+ rules = select_rules(
189
+ registry,
190
+ profile=profile,
191
+ select=selected_rules,
192
+ ignore=ignored_rules,
193
+ )
194
+ baseline = Baseline.load(baseline_path) if baseline_path is not None else None
195
+ except (ProfileError, UnknownRuleError, BaselineError) as error:
196
+ err_console.print(str(error))
197
+ raise typer.Exit(EXIT_INTERNAL_ERROR) from None
198
+
199
+ check_run = _Check(
200
+ paths=paths,
201
+ rules=rules,
202
+ profile=profile.resolve(registry.default_params()),
203
+ baseline=baseline,
204
+ ignored=ignored_internal | (profile.disabled & frozenset(INTERNAL)),
205
+ known_ids=frozenset(impl.id for impl in registry) | frozenset(INTERNAL),
206
+ # Снимок фиксируется по всем находкам: иначе его содержимое зависело бы
207
+ # от ключа --severity, с которым его записали.
208
+ threshold=Severity.INFO if write_baseline is not None else severity,
209
+ )
210
+ result = check_run()
211
+
212
+ if write_baseline is not None:
213
+ _write_baseline(write_baseline, result)
214
+ raise typer.Exit(EXIT_OK)
215
+
216
+ if show_diff:
217
+ sys.stdout.write(_diff(check_run, result))
218
+ raise typer.Exit(EXIT_OK)
219
+
220
+ if fix:
221
+ result = _apply_fixes(check_run, result)
222
+
223
+ if not quiet:
224
+ _report(result, output_format, limit)
225
+ raise typer.Exit(EXIT_FOUND_ERRORS if result.has_errors else EXIT_OK)
226
+
227
+
228
+ @dataclass(frozen=True, slots=True)
229
+ class _Check:
230
+ """Один прогон проверки. Ключ ``--fix`` повторяет его после каждой правки."""
231
+
232
+ paths: list[Path]
233
+ rules: tuple[RuleImpl, ...]
234
+ profile: Profile
235
+ baseline: Baseline | None
236
+ ignored: frozenset[str]
237
+ known_ids: frozenset[str]
238
+ threshold: Severity
239
+
240
+ def __call__(self, overlay: dict[Path, str] | None = None) -> RunResult:
241
+ parsed = parse(self.paths, self.profile, overlay)
242
+ return run(
243
+ parsed.document,
244
+ self.rules,
245
+ extra_findings=parse_findings(parsed),
246
+ threshold=self.threshold,
247
+ suppressions=parsed.suppressions,
248
+ baseline=self.baseline,
249
+ ignored=self.ignored,
250
+ known_ids=self.known_ids,
251
+ )
252
+
253
+
254
+ def _diff(check_run: _Check, result: RunResult) -> str:
255
+ """Показать всё, что сделал бы ``--fix``, ничего не записывая."""
256
+ overlay: dict[Path, str] = {}
257
+ for _ in range(FIX_PASSES):
258
+ prepared = plan(result.findings, overlay)
259
+ if not prepared.applied:
260
+ break
261
+ overlay.update({edit.path: edit.text for edit in prepared.edits})
262
+ result = check_run(overlay)
263
+ return diff(overlay)
264
+
265
+
266
+ def _apply_fixes(check_run: _Check, result: RunResult) -> RunResult:
267
+ """Применять правки, пока они находятся, и перепроверять исходники.
268
+
269
+ Проходов несколько: пересекающиеся правки в один проход не применяются,
270
+ а исправленное место может открыть следующее нарушение.
271
+ """
272
+ applied = 0
273
+ reported: set[Path] = set()
274
+ for _ in range(FIX_PASSES):
275
+ prepared = plan(result.findings)
276
+ for path in prepared.skipped:
277
+ if path not in reported:
278
+ reported.add(path)
279
+ err_console.print(f"Не удалось прочитать как UTF-8, пропущен: {path}")
280
+ if not prepared.applied:
281
+ break
282
+ applied += write(prepared)
283
+ result = check_run()
284
+
285
+ if applied:
286
+ console.print(f"Исправлено находок: {applied}.")
287
+ return result
288
+
289
+
290
+ def _write_baseline(path: Path, result: RunResult) -> None:
291
+ snapshot = Baseline.of(result.findings)
292
+ path.parent.mkdir(parents=True, exist_ok=True)
293
+ path.write_text(snapshot.dumps(__version__), encoding="utf-8")
294
+ console.print(f"Записано находок в снимок: {len(result.findings)} → {path}")
295
+
296
+
297
+ def _report(result: RunResult, output_format: OutputFormat, limit: int) -> None:
298
+ # agent и json пишутся в stdout напрямую: Rich переносил бы длинные строки,
299
+ # а оба формата копируются и разбираются как есть.
300
+ if output_format is OutputFormat.JSON:
301
+ sys.stdout.write(json_report.render(result))
302
+ elif output_format is OutputFormat.AGENT:
303
+ command = " ".join(["nk", *sys.argv[1:]])
304
+ sys.stdout.write(agent.render(result, command=command, limit=limit))
305
+ else:
306
+ human.render(result, console)
307
+
308
+
309
+ def _split(value: str | None) -> list[str] | None:
310
+ if value is None:
311
+ return None
312
+ return [item.strip() for item in value.split(",") if item.strip()]
313
+
314
+
315
+ @rules_app.command("docs")
316
+ def rules_docs_command(
317
+ output: Path = typer.Option(
318
+ Path("docs/rules"), "--output", "-o", help="Каталог, куда записать страницы правил."
319
+ ),
320
+ fixtures: Path = typer.Option(
321
+ examples.DEFAULT_FIXTURES_ROOT,
322
+ "--fixtures",
323
+ help="Каталог фикстур, из которых берутся примеры.",
324
+ ),
325
+ ) -> None:
326
+ """Сгенерировать страницы документации по правилам."""
327
+ registry = load_rules()
328
+ written, removed = rules_docs.write_pages(registry, output, fixtures_root=fixtures)
329
+ console.print(f"Записано страниц: {written} → {output}")
330
+ if removed:
331
+ console.print(f"Удалено устаревших: {removed}")
nk/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Ядро: контракты находки, правила, документа и профиля."""
nk/core/baseline.py ADDED
@@ -0,0 +1,94 @@
1
+ """Снимок известных нарушений.
2
+
3
+ Линтер, включённый на готовой работе, выдаёт сотни находок сразу. Снимок фиксирует
4
+ текущее состояние, и дальше показываются только новые нарушения: работа чинится
5
+ постепенно, а не «когда-нибудь потом».
6
+
7
+ Находка опознаётся не номером строки, а содержимым строки нарушения: отчёт растёт
8
+ весь семестр, и вставка абзаца выше не должна воскрешать все находки ниже. Если же
9
+ саму строку правили — находка всплывёт снова, и это правильно.
10
+ """
11
+
12
+ import json
13
+ from collections import Counter
14
+ from dataclasses import dataclass, field
15
+ from hashlib import sha256
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from nk.core.finding import Finding
20
+
21
+ SCHEMA_VERSION = "1.0"
22
+ TOOL_NAME = "nk"
23
+ FINGERPRINT_LENGTH = 16
24
+
25
+ #: Путь, ключ и отпечаток строки нарушения.
26
+ Key = tuple[str, str, str]
27
+
28
+
29
+ class BaselineError(ValueError):
30
+ """Снимок не читается или испорчен."""
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class Baseline:
35
+ counts: dict[Key, int] = field(default_factory=dict)
36
+
37
+ @classmethod
38
+ def of(cls, findings: tuple[Finding, ...]) -> "Baseline":
39
+ return cls(dict(Counter(fingerprint(finding) for finding in findings)))
40
+
41
+ @classmethod
42
+ def load(cls, path: Path) -> "Baseline":
43
+ try:
44
+ data = json.loads(path.read_text(encoding="utf-8"))
45
+ except OSError as error:
46
+ raise BaselineError(f"не удалось прочитать снимок {path}: {error}") from error
47
+ except json.JSONDecodeError as error:
48
+ raise BaselineError(f"снимок {path} испорчен: {error}") from error
49
+
50
+ version = data.get("schema_version")
51
+ if version != SCHEMA_VERSION:
52
+ raise BaselineError(
53
+ f"снимок {path} версии {version!r}, поддерживается {SCHEMA_VERSION!r}"
54
+ )
55
+ try:
56
+ counts = {
57
+ (entry["path"], entry["rule_id"], entry["fingerprint"]): int(entry["count"])
58
+ for entry in data["entries"]
59
+ }
60
+ except (KeyError, TypeError, ValueError) as error:
61
+ raise BaselineError(f"снимок {path}: неожиданная структура записи ({error})") from error
62
+ return cls(counts)
63
+
64
+ def dumps(self, version: str) -> str:
65
+ entries = [
66
+ {"path": path, "rule_id": rule_id, "fingerprint": digest, "count": count}
67
+ for (path, rule_id, digest), count in sorted(self.counts.items())
68
+ ]
69
+ payload: dict[str, Any] = {
70
+ "schema_version": SCHEMA_VERSION,
71
+ "tool": {"name": TOOL_NAME, "version": version},
72
+ "entries": entries,
73
+ }
74
+ return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
75
+
76
+ def filter(self, findings: tuple[Finding, ...]) -> tuple[tuple[Finding, ...], int]:
77
+ """Отсеять находки, уже учтённые снимком."""
78
+ remaining = dict(self.counts)
79
+ kept: list[Finding] = []
80
+ for finding in findings:
81
+ key = fingerprint(finding)
82
+ available = remaining.get(key, 0)
83
+ if available:
84
+ remaining[key] = available - 1
85
+ continue
86
+ kept.append(finding)
87
+ return tuple(kept), len(findings) - len(kept)
88
+
89
+
90
+ def fingerprint(finding: Finding) -> Key:
91
+ """Ключ находки, устойчивый к сдвигу строк."""
92
+ excerpt = " ".join((finding.excerpt or "").split())
93
+ digest = sha256(excerpt.encode("utf-8")).hexdigest()[:FINGERPRINT_LENGTH]
94
+ return (str(finding.path), finding.rule_id, digest)
nk/core/diagnostics.py ADDED
@@ -0,0 +1,44 @@
1
+ """Внутренние диагностики линтера.
2
+
3
+ Это не правила: они не проверяют требований стандарта, не имеют пункта и фикстур.
4
+ Но отключать их профилем и ключом ``--ignore`` нужно так же, как правила, поэтому
5
+ их идентификаторы перечислены здесь.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from nk.core.finding import Severity
11
+
12
+ #: Диагностики не ссылаются на пункт стандарта.
13
+ NO_CLAUSE = ""
14
+
15
+ INPUT_MISSING = "NK-PARSE-001"
16
+ INPUT_CYCLE = "NK-PARSE-002"
17
+ ENVIRONMENT_UNCLOSED = "NK-PARSE-003"
18
+ ENVIRONMENT_ORPHAN_END = "NK-PARSE-004"
19
+ GROUP_UNCLOSED = "NK-PARSE-005"
20
+ ENCODING_FALLBACK = "NK-PARSE-006"
21
+ IGNORE_UNUSED = "NK-IGNORE-001"
22
+ IGNORE_UNKNOWN = "NK-IGNORE-002"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Diagnostic:
27
+ code: str
28
+ severity: Severity
29
+ title: str
30
+
31
+
32
+ INTERNAL: dict[str, Diagnostic] = {
33
+ item.code: item
34
+ for item in (
35
+ Diagnostic(INPUT_MISSING, Severity.INFO, "Включаемый файл не найден"),
36
+ Diagnostic(INPUT_CYCLE, Severity.INFO, "Циклическое включение файла"),
37
+ Diagnostic(ENVIRONMENT_UNCLOSED, Severity.INFO, "Окружение не закрыто"),
38
+ Diagnostic(ENVIRONMENT_ORPHAN_END, Severity.INFO, "\\end без парного \\begin"),
39
+ Diagnostic(GROUP_UNCLOSED, Severity.INFO, "Не закрыта скобка аргумента"),
40
+ Diagnostic(ENCODING_FALLBACK, Severity.INFO, "Файл не в UTF-8"),
41
+ Diagnostic(IGNORE_UNUSED, Severity.WARNING, "Подавление ничего не подавило"),
42
+ Diagnostic(IGNORE_UNKNOWN, Severity.WARNING, "Подавление ссылается на неизвестное правило"),
43
+ )
44
+ }