code-standards 7.0.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.
- code_standards-7.0.0.dist-info/METADATA +53 -0
- code_standards-7.0.0.dist-info/RECORD +99 -0
- code_standards-7.0.0.dist-info/WHEEL +4 -0
- code_standards-7.0.0.dist-info/entry_points.txt +3 -0
- code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
- sarj_standards/__init__.py +30 -0
- sarj_standards/__main__.py +5 -0
- sarj_standards/_meta.py +22 -0
- sarj_standards/api.py +890 -0
- sarj_standards/cli/__init__.py +0 -0
- sarj_standards/cli/main.py +2466 -0
- sarj_standards/configs/cli-reference.v1.json +1 -0
- sarj_standards/configs/doctor.config.json +22 -0
- sarj_standards/configs/eslint.application.mjs +1366 -0
- sarj_standards/configs/eslint.peers.json +44 -0
- sarj_standards/configs/eslint.strict.mjs +1060 -0
- sarj_standards/configs/markdownlint.strict.yaml +12 -0
- sarj_standards/configs/pyright.strict.json +96 -0
- sarj_standards/configs/ruff.application.toml +363 -0
- sarj_standards/configs/ruff.strict.toml +338 -0
- sarj_standards/configs/rule-inventory.v1.json +1 -0
- sarj_standards/configs/rule-ledger.json +846 -0
- sarj_standards/configs/rule-warning-levels.v1.json +1 -0
- sarj_standards/configs/taplo.strict.toml +14 -0
- sarj_standards/configs/yamllint.strict.yaml +25 -0
- sarj_standards/libs/__init__.py +0 -0
- sarj_standards/libs/adoption/__init__.py +0 -0
- sarj_standards/libs/adoption/configs.py +36 -0
- sarj_standards/libs/adoption/doctor.py +1346 -0
- sarj_standards/libs/adoption/exclusions.py +66 -0
- sarj_standards/libs/adoption/hooks.py +423 -0
- sarj_standards/libs/adoption/launcher.py +240 -0
- sarj_standards/libs/adoption/lifecycle.py +493 -0
- sarj_standards/libs/adoption/manifest.py +550 -0
- sarj_standards/libs/adoption/packagemanager.py +285 -0
- sarj_standards/libs/adoption/retired_suppressions.py +371 -0
- sarj_standards/libs/adoption/scaffold.py +1660 -0
- sarj_standards/libs/adoption/service.py +441 -0
- sarj_standards/libs/adoption/transaction.py +274 -0
- sarj_standards/libs/adoption/upgrade.py +516 -0
- sarj_standards/libs/adoption/uvtool.py +62 -0
- sarj_standards/libs/catalogs/__init__.py +9 -0
- sarj_standards/libs/catalogs/slack_automations.py +627 -0
- sarj_standards/libs/corpus/__init__.py +25 -0
- sarj_standards/libs/corpus/manifest.py +211 -0
- sarj_standards/libs/corpus/snapshot.py +222 -0
- sarj_standards/libs/diagnostics/__init__.py +65 -0
- sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
- sarj_standards/libs/diagnostics/baseline.py +131 -0
- sarj_standards/libs/diagnostics/models.py +574 -0
- sarj_standards/libs/diagnostics/serialize.py +290 -0
- sarj_standards/libs/diagnostics/source.py +172 -0
- sarj_standards/libs/filesystem.py +11 -0
- sarj_standards/libs/linting/__init__.py +0 -0
- sarj_standards/libs/linting/analysis.py +422 -0
- sarj_standards/libs/linting/external.py +1454 -0
- sarj_standards/libs/linting/library_policy.py +688 -0
- sarj_standards/libs/linting/policy.py +152 -0
- sarj_standards/libs/linting/runner.py +442 -0
- sarj_standards/libs/linting/textlint.py +1605 -0
- sarj_standards/libs/release/__init__.py +98 -0
- sarj_standards/libs/release/_values.py +24 -0
- sarj_standards/libs/release/artifacts.py +191 -0
- sarj_standards/libs/release/causality.py +80 -0
- sarj_standards/libs/release/changes.py +48 -0
- sarj_standards/libs/release/process.py +128 -0
- sarj_standards/libs/release/publish.py +85 -0
- sarj_standards/libs/release/registry.py +271 -0
- sarj_standards/libs/release/release_age.py +218 -0
- sarj_standards/libs/release/rollout.py +1163 -0
- sarj_standards/libs/release/tags.py +373 -0
- sarj_standards/libs/release/typescript.py +191 -0
- sarj_standards/libs/repository/__init__.py +0 -0
- sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
- sarj_standards/libs/repository/comment_corpus.py +536 -0
- sarj_standards/libs/repository/config_generation.py +146 -0
- sarj_standards/libs/repository/docs.py +347 -0
- sarj_standards/libs/repository/hooks.py +118 -0
- sarj_standards/libs/repository/ledger.py +99 -0
- sarj_standards/libs/repository/repository.py +744 -0
- sarj_standards/libs/repository/rule_authoring.py +246 -0
- sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
- sarj_standards/libs/repository/rule_changes.py +318 -0
- sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
- sarj_standards/libs/repository/rule_lifecycle.py +167 -0
- sarj_standards/libs/repository/rule_maintenance.py +225 -0
- sarj_standards/libs/rules/__init__.py +74 -0
- sarj_standards/libs/rules/catalog.py +145 -0
- sarj_standards/libs/rules/contracts.py +382 -0
- sarj_standards/libs/rules/corpus_runner.py +365 -0
- sarj_standards/libs/rules/evaluation.py +177 -0
- sarj_standards/libs/setup/__init__.py +4 -0
- sarj_standards/libs/setup/repository.py +40 -0
- sarj_standards/py.typed +0 -0
- sarj_standards/schemas/__init__.py +4 -0
- sarj_standards/schemas/_paths.py +7 -0
- sarj_standards/schemas/rule-catalog.v1.json +1 -0
- sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
- sarj_standards/schemas/slack-automations.v1.schema.json +1751 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from sarj_standards.libs.diagnostics import (
|
|
10
|
+
AnalysisReport,
|
|
11
|
+
AnalyzerId,
|
|
12
|
+
Completion,
|
|
13
|
+
Conclusion,
|
|
14
|
+
Diagnostic,
|
|
15
|
+
ExecutionIssue,
|
|
16
|
+
InvocationId,
|
|
17
|
+
Location,
|
|
18
|
+
Severity,
|
|
19
|
+
SourceDocument,
|
|
20
|
+
ToolReport,
|
|
21
|
+
diagnostic_fingerprint,
|
|
22
|
+
)
|
|
23
|
+
from sarj_standards.libs.rules import RuleEngine, RuleSelection
|
|
24
|
+
|
|
25
|
+
from . import textlint
|
|
26
|
+
from .runner import GroupedPaths, group_paths
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Mapping, Sequence
|
|
31
|
+
|
|
32
|
+
from .policy import Policy
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@runtime_checkable
|
|
36
|
+
class _NativeDiagnostic(Protocol):
|
|
37
|
+
@property
|
|
38
|
+
def path(self) -> Path: ...
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def line(self) -> int: ...
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def col(self) -> int: ...
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def code(self) -> str: ...
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def message(self) -> str: ...
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def column_encoding(self) -> object: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class _SeverityDiagnostic(_NativeDiagnostic, Protocol):
|
|
58
|
+
@property
|
|
59
|
+
def severity(self) -> object: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _RuleMetadata(Protocol):
|
|
63
|
+
code: str
|
|
64
|
+
id: str
|
|
65
|
+
description: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@runtime_checkable
|
|
69
|
+
class _CheckerModule(Protocol):
|
|
70
|
+
def analyze(self, rule_ids: list[str], paths: list[Path]) -> list[_NativeDiagnostic]: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class _RegistryModule(Protocol):
|
|
75
|
+
REGISTRY: Mapping[str, type[_RuleMetadata]]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _LoadedNative(NamedTuple):
|
|
79
|
+
checker: _CheckerModule
|
|
80
|
+
registry: _RegistryModule
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _NativeRuleMetadata(NamedTuple):
|
|
84
|
+
rule_id: str
|
|
85
|
+
description: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def analyze(
|
|
89
|
+
files: Sequence[str],
|
|
90
|
+
*,
|
|
91
|
+
root: Path,
|
|
92
|
+
python_baseline: Path | None = None,
|
|
93
|
+
policy: Policy | None = None,
|
|
94
|
+
grouped: GroupedPaths | None = None,
|
|
95
|
+
rule_selection: RuleSelection | None = None,
|
|
96
|
+
) -> AnalysisReport:
|
|
97
|
+
root = root.resolve()
|
|
98
|
+
try:
|
|
99
|
+
contained = tuple(_contained_path(item, root) for item in files)
|
|
100
|
+
routed = grouped if grouped is not None else group_paths(contained, policy=policy)
|
|
101
|
+
except (OSError, ValueError) as exc:
|
|
102
|
+
issue = ExecutionIssue("sarj-standards", "invalid-input", str(exc).replace(str(root), "."))
|
|
103
|
+
tool = ToolReport("sarj-standards", Completion.FAILED, issues=(issue,))
|
|
104
|
+
return AnalysisReport(root, Completion.FAILED, Conclusion.INCONCLUSIVE, (tool,))
|
|
105
|
+
|
|
106
|
+
reports = tuple(
|
|
107
|
+
report
|
|
108
|
+
for report in (
|
|
109
|
+
_native_report(
|
|
110
|
+
"sarj-python-lint",
|
|
111
|
+
"sarj_python_lint",
|
|
112
|
+
routed.python,
|
|
113
|
+
root,
|
|
114
|
+
python_baseline=python_baseline,
|
|
115
|
+
rule_ids=_selected_ids(rule_selection, RuleEngine.PYTHON),
|
|
116
|
+
),
|
|
117
|
+
_native_report(
|
|
118
|
+
"sarj-sql-lint",
|
|
119
|
+
"sarj_sql_lint",
|
|
120
|
+
routed.sql,
|
|
121
|
+
root,
|
|
122
|
+
rule_ids=_selected_ids(rule_selection, RuleEngine.SQL),
|
|
123
|
+
),
|
|
124
|
+
_native_report(
|
|
125
|
+
"sarj-iac-lint",
|
|
126
|
+
"sarj_iac_lint",
|
|
127
|
+
routed.iac,
|
|
128
|
+
root,
|
|
129
|
+
rule_ids=_selected_ids(rule_selection, RuleEngine.IAC),
|
|
130
|
+
),
|
|
131
|
+
_text_report(
|
|
132
|
+
routed,
|
|
133
|
+
root,
|
|
134
|
+
rule_ids=_selected_ids(rule_selection, RuleEngine.TEXT),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
if report is not None
|
|
138
|
+
)
|
|
139
|
+
report = report_from_tools(root, reports)
|
|
140
|
+
if policy is None:
|
|
141
|
+
return report
|
|
142
|
+
filtered = tuple(
|
|
143
|
+
ToolReport(
|
|
144
|
+
item.name,
|
|
145
|
+
item.completion,
|
|
146
|
+
diagnostics=policy.filter_diagnostics(item.diagnostics),
|
|
147
|
+
issues=item.issues,
|
|
148
|
+
analyzer_id=item.analyzer_id,
|
|
149
|
+
invocation_id=item.invocation_id,
|
|
150
|
+
version=item.version,
|
|
151
|
+
duration_ms=item.duration_ms,
|
|
152
|
+
file_count=item.file_count,
|
|
153
|
+
cache_status=item.cache_status,
|
|
154
|
+
)
|
|
155
|
+
for item in report.tools
|
|
156
|
+
)
|
|
157
|
+
return report_from_tools(root, filtered)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _selected_ids(selection: RuleSelection | None, engine: RuleEngine) -> frozenset[str] | None:
|
|
161
|
+
if selection is None:
|
|
162
|
+
return None
|
|
163
|
+
return frozenset(str(value) for value in selection.ids_for(engine))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _contained_path(value: str, root: Path) -> str:
|
|
167
|
+
path = Path(value)
|
|
168
|
+
resolved = (path if path.is_absolute() else root / path).resolve()
|
|
169
|
+
try:
|
|
170
|
+
resolved.relative_to(root)
|
|
171
|
+
except ValueError as exc:
|
|
172
|
+
msg = "analysis path is outside the repository root"
|
|
173
|
+
raise ValueError(msg) from exc
|
|
174
|
+
return str(resolved)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def report_from_tools(root: Path, reports: Sequence[ToolReport]) -> AnalysisReport:
|
|
178
|
+
normalized = tuple(
|
|
179
|
+
ToolReport(
|
|
180
|
+
report.name,
|
|
181
|
+
report.completion,
|
|
182
|
+
diagnostics=tuple(
|
|
183
|
+
sorted(
|
|
184
|
+
(
|
|
185
|
+
item
|
|
186
|
+
if item.fingerprint is not None
|
|
187
|
+
else replace(item, fingerprint=diagnostic_fingerprint(item, anchor=item.message))
|
|
188
|
+
for item in report.diagnostics
|
|
189
|
+
),
|
|
190
|
+
key=_diagnostic_key,
|
|
191
|
+
)
|
|
192
|
+
),
|
|
193
|
+
issues=tuple(sorted(report.issues, key=lambda issue: (issue.source, issue.kind, issue.message))),
|
|
194
|
+
analyzer_id=report.analyzer_id,
|
|
195
|
+
invocation_id=report.invocation_id,
|
|
196
|
+
version=report.version,
|
|
197
|
+
duration_ms=report.duration_ms,
|
|
198
|
+
file_count=report.file_count,
|
|
199
|
+
cache_status=report.cache_status,
|
|
200
|
+
)
|
|
201
|
+
for report in sorted(reports, key=lambda report: report.name)
|
|
202
|
+
)
|
|
203
|
+
issues = tuple(issue for report in normalized for issue in report.issues)
|
|
204
|
+
diagnostics = tuple(item for report in normalized for item in report.diagnostics)
|
|
205
|
+
if issues:
|
|
206
|
+
completion = (
|
|
207
|
+
Completion.FAILED
|
|
208
|
+
if normalized and all(report.completion is Completion.FAILED for report in normalized)
|
|
209
|
+
else Completion.PARTIAL
|
|
210
|
+
)
|
|
211
|
+
conclusion = Conclusion.FINDINGS if diagnostics else Conclusion.INCONCLUSIVE
|
|
212
|
+
else:
|
|
213
|
+
completion = Completion.COMPLETE
|
|
214
|
+
conclusion = Conclusion.FINDINGS if diagnostics else Conclusion.PASSED
|
|
215
|
+
return AnalysisReport(root, completion, conclusion, normalized)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _diagnostic_key(diagnostic: Diagnostic) -> tuple[object, ...]:
|
|
219
|
+
location = diagnostic.location
|
|
220
|
+
position = location.region.start if location.region is not None else location.position
|
|
221
|
+
end = location.region.end if location.region is not None else None
|
|
222
|
+
return (
|
|
223
|
+
diagnostic.source,
|
|
224
|
+
diagnostic.code,
|
|
225
|
+
location.path,
|
|
226
|
+
-1 if position is None else position.line,
|
|
227
|
+
-1 if position is None else position.character,
|
|
228
|
+
-1 if end is None else end.line,
|
|
229
|
+
-1 if end is None else end.character,
|
|
230
|
+
diagnostic.severity.value,
|
|
231
|
+
diagnostic.message,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _native_report(
|
|
236
|
+
name: str,
|
|
237
|
+
package: str,
|
|
238
|
+
files: Sequence[str],
|
|
239
|
+
root: Path,
|
|
240
|
+
*,
|
|
241
|
+
python_baseline: Path | None = None,
|
|
242
|
+
rule_ids: frozenset[str] | None = None,
|
|
243
|
+
) -> ToolReport | None:
|
|
244
|
+
if not files or rule_ids == frozenset():
|
|
245
|
+
return None
|
|
246
|
+
try:
|
|
247
|
+
return _run_native(name, package, files, root, python_baseline=python_baseline, rule_ids=rule_ids)
|
|
248
|
+
except Exception as exc: # ruff: ignore[blind-except] -- one analyzer failure must not erase other tool results.
|
|
249
|
+
issue = ExecutionIssue(name, "analyzer-failure", f"{type(exc).__name__}: {exc}")
|
|
250
|
+
return ToolReport(name, Completion.FAILED, issues=(issue,))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_native(
|
|
254
|
+
name: str,
|
|
255
|
+
package: str,
|
|
256
|
+
files: Sequence[str],
|
|
257
|
+
root: Path,
|
|
258
|
+
*,
|
|
259
|
+
python_baseline: Path | None,
|
|
260
|
+
rule_ids: frozenset[str] | None,
|
|
261
|
+
) -> ToolReport:
|
|
262
|
+
started = time.monotonic()
|
|
263
|
+
checker_module, registry_module = _load_native(package)
|
|
264
|
+
metadata = _metadata(registry_module.REGISTRY)
|
|
265
|
+
selected_rules = sorted(registry_module.REGISTRY) if rule_ids is None else sorted(rule_ids)
|
|
266
|
+
unknown = sorted(set(selected_rules) - registry_module.REGISTRY.keys())
|
|
267
|
+
if unknown:
|
|
268
|
+
msg = f"unknown {package} rule(s): {', '.join(unknown)}"
|
|
269
|
+
raise ValueError(msg)
|
|
270
|
+
paths = [Path(item) for item in files]
|
|
271
|
+
native_result: Sequence[_NativeDiagnostic]
|
|
272
|
+
if package == "sarj_python_lint":
|
|
273
|
+
from sarj_python_lint.__main__ import analyze as analyze_python # ruff: ignore[import-outside-top-level]
|
|
274
|
+
|
|
275
|
+
native_result = analyze_python(
|
|
276
|
+
selected_rules,
|
|
277
|
+
paths,
|
|
278
|
+
baseline=python_baseline,
|
|
279
|
+
root=root,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
native_result = checker_module.analyze(selected_rules, paths)
|
|
283
|
+
cache: dict[Path, SourceDocument | None] = {}
|
|
284
|
+
diagnostics = tuple(
|
|
285
|
+
_normalize_native(item, source=name, root=root, metadata=metadata, documents=cache) for item in native_result
|
|
286
|
+
)
|
|
287
|
+
return ToolReport(
|
|
288
|
+
name,
|
|
289
|
+
Completion.COMPLETE,
|
|
290
|
+
diagnostics=diagnostics,
|
|
291
|
+
analyzer_id=AnalyzerId(name),
|
|
292
|
+
invocation_id=InvocationId(name),
|
|
293
|
+
duration_ms=round((time.monotonic() - started) * 1_000),
|
|
294
|
+
file_count=len(files),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _load_native(package: str) -> _LoadedNative:
|
|
299
|
+
checker_module = import_module(f"{package}.__main__")
|
|
300
|
+
registry_module = import_module(f"{package}.rules")
|
|
301
|
+
if not isinstance(checker_module, _CheckerModule) or not isinstance(registry_module, _RegistryModule):
|
|
302
|
+
msg = f"{package} does not expose the expected in-process analysis API"
|
|
303
|
+
raise TypeError(msg)
|
|
304
|
+
return _LoadedNative(checker_module, registry_module)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _metadata(registry: Mapping[str, type[_RuleMetadata]]) -> dict[str, _NativeRuleMetadata]:
|
|
308
|
+
by_code: dict[str, _NativeRuleMetadata] = {}
|
|
309
|
+
for rule_id, rule_type in registry.items():
|
|
310
|
+
rule = rule_type()
|
|
311
|
+
by_code[rule.code] = _NativeRuleMetadata(rule_id, rule.description)
|
|
312
|
+
return by_code
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _normalize_native(
|
|
316
|
+
item: _NativeDiagnostic,
|
|
317
|
+
*,
|
|
318
|
+
source: str,
|
|
319
|
+
root: Path,
|
|
320
|
+
metadata: Mapping[str, _NativeRuleMetadata],
|
|
321
|
+
documents: dict[Path, SourceDocument | None],
|
|
322
|
+
) -> Diagnostic:
|
|
323
|
+
resolved = item.path.resolve()
|
|
324
|
+
document = documents.get(resolved)
|
|
325
|
+
if resolved not in documents:
|
|
326
|
+
try:
|
|
327
|
+
document = SourceDocument.read(resolved)
|
|
328
|
+
except OSError:
|
|
329
|
+
document = None
|
|
330
|
+
documents[resolved] = document
|
|
331
|
+
position = None
|
|
332
|
+
if document is not None:
|
|
333
|
+
position = (
|
|
334
|
+
document.byte_point(line=item.line, column=item.col)
|
|
335
|
+
if source == "sarj-python-lint" and str(item.column_encoding) == "utf8-bytes"
|
|
336
|
+
else document.point(line=item.line, column=item.col)
|
|
337
|
+
)
|
|
338
|
+
rule_id, help_text = metadata.get(item.code, (None, None))
|
|
339
|
+
return Diagnostic(
|
|
340
|
+
code=item.code,
|
|
341
|
+
rule_id=rule_id,
|
|
342
|
+
message=item.message,
|
|
343
|
+
severity=_severity(item),
|
|
344
|
+
source=source,
|
|
345
|
+
location=Location(_relative_path(resolved, root), position=position),
|
|
346
|
+
help=help_text,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _severity(item: _NativeDiagnostic) -> Severity:
|
|
351
|
+
if isinstance(item, _SeverityDiagnostic) and str(item.severity) == "warning":
|
|
352
|
+
return Severity.WARNING
|
|
353
|
+
return Severity.ERROR
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _text_report(
|
|
357
|
+
grouped: GroupedPaths,
|
|
358
|
+
root: Path,
|
|
359
|
+
*,
|
|
360
|
+
rule_ids: frozenset[str] | None = None,
|
|
361
|
+
) -> ToolReport | None:
|
|
362
|
+
if not grouped.text or rule_ids == frozenset():
|
|
363
|
+
return None
|
|
364
|
+
try:
|
|
365
|
+
return _run_text(grouped.text, root, rule_ids=rule_ids)
|
|
366
|
+
except Exception as exc: # ruff: ignore[blind-except] -- retain results from independent analyzers.
|
|
367
|
+
issue = ExecutionIssue("sarj-text-lint", "analyzer-failure", f"{type(exc).__name__}: {exc}")
|
|
368
|
+
return ToolReport("sarj-text-lint", Completion.FAILED, issues=(issue,))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _run_text(files: Sequence[str], root: Path, *, rule_ids: frozenset[str] | None) -> ToolReport:
|
|
372
|
+
selected = frozenset(textlint.REGISTRY) if rule_ids is None else rule_ids
|
|
373
|
+
unknown = sorted(selected - textlint.REGISTRY.keys())
|
|
374
|
+
if unknown:
|
|
375
|
+
msg = f"unknown text rule(s): {', '.join(unknown)}"
|
|
376
|
+
raise ValueError(msg)
|
|
377
|
+
raw = tuple(
|
|
378
|
+
finding
|
|
379
|
+
for finding in textlint.check_paths(files, root=root)
|
|
380
|
+
if any(meta.code == finding.code and rule_id in selected for rule_id, meta in textlint.REGISTRY.items())
|
|
381
|
+
)
|
|
382
|
+
by_code = {meta.code: (rule_id, meta.description, meta.blocking) for rule_id, meta in textlint.REGISTRY.items()}
|
|
383
|
+
documents: dict[Path, SourceDocument | None] = {}
|
|
384
|
+
diagnostics = tuple(_normalize_text(item, root, by_code, documents) for item in raw)
|
|
385
|
+
return ToolReport("sarj-text-lint", Completion.COMPLETE, diagnostics=diagnostics)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _normalize_text(
|
|
389
|
+
item: textlint.Finding,
|
|
390
|
+
root: Path,
|
|
391
|
+
metadata: Mapping[str, tuple[str, str, bool]],
|
|
392
|
+
documents: dict[Path, SourceDocument | None],
|
|
393
|
+
) -> Diagnostic:
|
|
394
|
+
resolved = item.path.resolve()
|
|
395
|
+
document = documents.get(resolved)
|
|
396
|
+
if resolved not in documents:
|
|
397
|
+
try:
|
|
398
|
+
document = SourceDocument.read(resolved)
|
|
399
|
+
except OSError:
|
|
400
|
+
document = None
|
|
401
|
+
documents[resolved] = document
|
|
402
|
+
rule_id, help_text, blocking = metadata[item.code]
|
|
403
|
+
return Diagnostic(
|
|
404
|
+
code=item.code,
|
|
405
|
+
rule_id=rule_id,
|
|
406
|
+
message=item.message,
|
|
407
|
+
severity=Severity.ERROR if blocking else Severity.WARNING,
|
|
408
|
+
source="sarj-text-lint",
|
|
409
|
+
location=Location(
|
|
410
|
+
_relative_path(resolved, root),
|
|
411
|
+
position=None if document is None else document.point(line=item.line, column=1),
|
|
412
|
+
),
|
|
413
|
+
help=help_text,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _relative_path(path: Path, root: Path) -> str:
|
|
418
|
+
try:
|
|
419
|
+
return path.relative_to(root).as_posix()
|
|
420
|
+
except ValueError as exc:
|
|
421
|
+
msg = "analyzer reported a path outside the repository root"
|
|
422
|
+
raise ValueError(msg) from exc
|