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
sarj_standards/api.py
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess # ruff: ignore[suspicious-subprocess-import] -- latest updates execute a fixed uvx argv.
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ._meta import (
|
|
12
|
+
__version__,
|
|
13
|
+
)
|
|
14
|
+
from .libs.adoption import launcher
|
|
15
|
+
from .libs.adoption.doctor import Finding as DoctorFinding
|
|
16
|
+
from .libs.adoption.doctor import Level as DoctorLevel
|
|
17
|
+
from .libs.adoption.doctor import diagnose
|
|
18
|
+
from .libs.adoption.lifecycle import (
|
|
19
|
+
Inspection,
|
|
20
|
+
execute,
|
|
21
|
+
inspect,
|
|
22
|
+
selected_eslint_commands,
|
|
23
|
+
verification_commands,
|
|
24
|
+
)
|
|
25
|
+
from .libs.adoption.manifest import load as load_manifest
|
|
26
|
+
from .libs.adoption.scaffold import detect
|
|
27
|
+
from .libs.adoption.service import (
|
|
28
|
+
InitPlan,
|
|
29
|
+
apply_init,
|
|
30
|
+
apply_sync,
|
|
31
|
+
plan_init,
|
|
32
|
+
plan_sync,
|
|
33
|
+
)
|
|
34
|
+
from .libs.diagnostics import (
|
|
35
|
+
AnalysisReport,
|
|
36
|
+
Completion,
|
|
37
|
+
Conclusion,
|
|
38
|
+
CoverageDisposition,
|
|
39
|
+
CoverageNotice,
|
|
40
|
+
Diagnostic,
|
|
41
|
+
ExecutionIssue,
|
|
42
|
+
Fix,
|
|
43
|
+
FixSafety,
|
|
44
|
+
Location,
|
|
45
|
+
Position,
|
|
46
|
+
Region,
|
|
47
|
+
RelatedLocation,
|
|
48
|
+
Severity,
|
|
49
|
+
SourceDocument,
|
|
50
|
+
TextEdit,
|
|
51
|
+
ToolReport,
|
|
52
|
+
TrustMode,
|
|
53
|
+
to_github,
|
|
54
|
+
to_json,
|
|
55
|
+
to_sarif,
|
|
56
|
+
to_text,
|
|
57
|
+
)
|
|
58
|
+
from .libs.diagnostics import baseline as diagnostic_baseline
|
|
59
|
+
from .libs.filesystem import is_link_like
|
|
60
|
+
from .libs.linting.analysis import analyze as analyze_paths
|
|
61
|
+
from .libs.linting.analysis import report_from_tools
|
|
62
|
+
from .libs.linting.external import analyze_external
|
|
63
|
+
from .libs.linting.library_policy import ManifestPolicyError
|
|
64
|
+
from .libs.linting.library_policy import accepts_path as library_policy_accepts_path
|
|
65
|
+
from .libs.linting.library_policy import scan as check_library_policy
|
|
66
|
+
from .libs.linting.library_policy import scan_paths as check_selected_library_policy
|
|
67
|
+
from .libs.linting.policy import Policy
|
|
68
|
+
from .libs.linting.runner import group_paths
|
|
69
|
+
from .libs.linting.runner import run as check
|
|
70
|
+
from .libs.rules import RuleEngine, RuleId, RuleSelection, RuleSelector
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if TYPE_CHECKING:
|
|
74
|
+
from collections.abc import Sequence
|
|
75
|
+
|
|
76
|
+
from .libs.adoption.manifest import Profile
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_INVALID_EXIT = 2
|
|
80
|
+
_INTERRUPTED_EXIT = 130
|
|
81
|
+
_INVALID_DOCTOR_FINDING_IDS = frozenset(
|
|
82
|
+
{"doctor.manifest.destination", "doctor.config.unknown", "doctor.package-json.invalid"}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Status(StrEnum):
|
|
87
|
+
OK = "ok"
|
|
88
|
+
CHANGED = "changed"
|
|
89
|
+
DRIFT = "drift"
|
|
90
|
+
INVALID = "invalid"
|
|
91
|
+
FAILED = "failed"
|
|
92
|
+
INTERRUPTED = "interrupted"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AnalysisMode(StrEnum):
|
|
96
|
+
POLICY = "policy"
|
|
97
|
+
CORPUS = "corpus"
|
|
98
|
+
OBSERVE = "observe"
|
|
99
|
+
RAW = "raw"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class Finding:
|
|
104
|
+
id: str
|
|
105
|
+
level: str
|
|
106
|
+
message: str
|
|
107
|
+
path: str | None = None
|
|
108
|
+
remediation: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class Change:
|
|
113
|
+
action: str
|
|
114
|
+
description: str
|
|
115
|
+
path: Path | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class Result:
|
|
120
|
+
status: Status
|
|
121
|
+
findings: tuple[Finding, ...] = ()
|
|
122
|
+
changes: tuple[Change, ...] = ()
|
|
123
|
+
exit_code: int = 0
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def ok(self) -> bool:
|
|
127
|
+
return self.exit_code == 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Standards:
|
|
131
|
+
def __init__(self, root: str | Path = ".") -> None:
|
|
132
|
+
resolved = Path(root).resolve()
|
|
133
|
+
if not resolved.is_dir():
|
|
134
|
+
msg = f"repository root {resolved} is not a directory"
|
|
135
|
+
raise ValueError(msg)
|
|
136
|
+
self.root: Path = resolved
|
|
137
|
+
|
|
138
|
+
def setup(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
profile: Profile | None = None,
|
|
142
|
+
configs: Sequence[str] | None = None,
|
|
143
|
+
python_root: str | None = None,
|
|
144
|
+
typescript_root: str | None = None,
|
|
145
|
+
force: bool = False,
|
|
146
|
+
install: bool = True,
|
|
147
|
+
dry_run: bool = False,
|
|
148
|
+
) -> Result:
|
|
149
|
+
try:
|
|
150
|
+
plan = plan_init(
|
|
151
|
+
self.root,
|
|
152
|
+
profile=profile,
|
|
153
|
+
configs=configs,
|
|
154
|
+
python_dest=python_root,
|
|
155
|
+
typescript_dest=typescript_root,
|
|
156
|
+
force=force,
|
|
157
|
+
)
|
|
158
|
+
except (KeyError, OSError, TypeError, ValueError) as exc:
|
|
159
|
+
return Result(
|
|
160
|
+
Status.INVALID,
|
|
161
|
+
findings=(Finding("setup.input.invalid", "error", str(exc)),),
|
|
162
|
+
exit_code=_INVALID_EXIT,
|
|
163
|
+
)
|
|
164
|
+
changes = _init_changes(plan, install=install)
|
|
165
|
+
if plan.scaffold.errors or plan.sync is None:
|
|
166
|
+
detail = "; ".join(plan.scaffold.errors) or "setup plan is not applicable"
|
|
167
|
+
return Result(
|
|
168
|
+
Status.INVALID,
|
|
169
|
+
findings=(Finding("setup.plan.invalid", "error", detail),),
|
|
170
|
+
changes=changes,
|
|
171
|
+
exit_code=_INVALID_EXIT,
|
|
172
|
+
)
|
|
173
|
+
if dry_run:
|
|
174
|
+
return Result(Status.CHANGED if changes else Status.OK, changes=changes)
|
|
175
|
+
applied = apply_init(plan, install=install)
|
|
176
|
+
findings = (
|
|
177
|
+
()
|
|
178
|
+
if applied.error is None
|
|
179
|
+
else (
|
|
180
|
+
Finding(
|
|
181
|
+
f"setup.{applied.failure.value if applied.failure is not None else 'apply'}.failed",
|
|
182
|
+
"error",
|
|
183
|
+
applied.error,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
if applied.status:
|
|
188
|
+
status = Status.INTERRUPTED if applied.status == _INTERRUPTED_EXIT else Status.FAILED
|
|
189
|
+
return Result(status, findings, changes, applied.status)
|
|
190
|
+
diagnosed = diagnose(self.root)
|
|
191
|
+
return _operation_result(_doctor_status(diagnosed), changes, findings=_doctor_findings(diagnosed))
|
|
192
|
+
|
|
193
|
+
def check(self, paths: Sequence[str] | None = None) -> Result:
|
|
194
|
+
if paths is not None:
|
|
195
|
+
try:
|
|
196
|
+
selected = _contained_paths(self.root, paths)
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
return Result(
|
|
199
|
+
Status.INVALID,
|
|
200
|
+
findings=(Finding("check.input.invalid", "error", str(exc)),),
|
|
201
|
+
exit_code=_INVALID_EXIT,
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
adopted = load_manifest(self.root)
|
|
205
|
+
policy = Policy.from_manifest(self.root, adopted)
|
|
206
|
+
selected = list(policy.filter_paths(selected))
|
|
207
|
+
except (OSError, TypeError, ValueError, ManifestPolicyError) as exc:
|
|
208
|
+
return Result(
|
|
209
|
+
Status.INVALID,
|
|
210
|
+
findings=(Finding("check.policy.invalid", "error", str(exc)),),
|
|
211
|
+
exit_code=_INVALID_EXIT,
|
|
212
|
+
)
|
|
213
|
+
try:
|
|
214
|
+
policy_findings = (
|
|
215
|
+
check_selected_library_policy(self.root, selected)
|
|
216
|
+
if adopted is not None and adopted.profile == "application"
|
|
217
|
+
else ()
|
|
218
|
+
)
|
|
219
|
+
except (OSError, TypeError, ValueError, ManifestPolicyError) as exc:
|
|
220
|
+
return Result(
|
|
221
|
+
Status.INVALID,
|
|
222
|
+
findings=(Finding("check.policy.invalid", "error", str(exc)),),
|
|
223
|
+
exit_code=_INVALID_EXIT,
|
|
224
|
+
)
|
|
225
|
+
findings = tuple(
|
|
226
|
+
Finding(item.id, "error", item.message, str(item.path), f"use {item.replacement}")
|
|
227
|
+
for item in policy_findings
|
|
228
|
+
)
|
|
229
|
+
source_status = check(selected, policy=policy)
|
|
230
|
+
eslint_status = execute(selected_eslint_commands(self.root, selected))
|
|
231
|
+
return _operation_result(max(source_status, eslint_status, 1 if findings else 0), findings=findings)
|
|
232
|
+
return _operation_result(_verify(self.root))
|
|
233
|
+
|
|
234
|
+
def analyze( # ruff: ignore[too-many-locals] -- one boundary coordinates routing, policy, and coverage.
|
|
235
|
+
self,
|
|
236
|
+
paths: Sequence[str] | None = None,
|
|
237
|
+
*,
|
|
238
|
+
external: bool = False,
|
|
239
|
+
trust: TrustMode | str = TrustMode.SAFE,
|
|
240
|
+
mode: AnalysisMode | str = AnalysisMode.POLICY,
|
|
241
|
+
rules: Sequence[str | RuleSelector] | None = None,
|
|
242
|
+
staged: bool = False,
|
|
243
|
+
) -> AnalysisReport:
|
|
244
|
+
try:
|
|
245
|
+
normalized_trust = TrustMode(trust)
|
|
246
|
+
normalized_mode = AnalysisMode(mode)
|
|
247
|
+
except ValueError as exc:
|
|
248
|
+
return _failed_analysis(self.root, "invalid-input", str(exc))
|
|
249
|
+
try:
|
|
250
|
+
adopted = load_manifest(self.root) if normalized_mode is not AnalysisMode.RAW else None
|
|
251
|
+
selection_policy = (
|
|
252
|
+
Policy.corpus_from_manifest(self.root, adopted)
|
|
253
|
+
if normalized_mode is AnalysisMode.CORPUS
|
|
254
|
+
else (
|
|
255
|
+
Policy.observe_from_manifest(self.root, adopted)
|
|
256
|
+
if normalized_mode is AnalysisMode.OBSERVE
|
|
257
|
+
else Policy.from_manifest(self.root, adopted)
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
rule_selection = _rule_selection(rules)
|
|
261
|
+
selected = _analysis_inputs(self.root, paths, mode=normalized_mode)
|
|
262
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
263
|
+
return _failed_analysis(self.root, "invalid-input", str(exc))
|
|
264
|
+
try:
|
|
265
|
+
baseline_counts: dict[str, int]
|
|
266
|
+
baseline_counts = (
|
|
267
|
+
diagnostic_baseline.load(self.root / adopted.diagnostic_baseline)
|
|
268
|
+
if normalized_mode is AnalysisMode.POLICY
|
|
269
|
+
and adopted is not None
|
|
270
|
+
and adopted.diagnostic_baseline is not None
|
|
271
|
+
else {}
|
|
272
|
+
)
|
|
273
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
274
|
+
return _failed_analysis(self.root, "baseline-failure", str(exc))
|
|
275
|
+
try:
|
|
276
|
+
active_selected = list(selection_policy.filter_paths(selected))
|
|
277
|
+
selected_groups = group_paths(active_selected, policy=selection_policy)
|
|
278
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
279
|
+
return _failed_analysis(self.root, "invalid-input", str(exc))
|
|
280
|
+
native = analyze_paths(
|
|
281
|
+
active_selected,
|
|
282
|
+
root=self.root,
|
|
283
|
+
policy=selection_policy,
|
|
284
|
+
grouped=selected_groups,
|
|
285
|
+
rule_selection=rule_selection,
|
|
286
|
+
)
|
|
287
|
+
if adopted is not None and adopted.profile == "application" and rule_selection is None:
|
|
288
|
+
try:
|
|
289
|
+
policy = _filter_tool_report(_policy_report(self.root, active_selected), selection_policy)
|
|
290
|
+
except (OSError, TypeError, ValueError, ManifestPolicyError) as exc:
|
|
291
|
+
issue = ExecutionIssue("sarj-library-policy", "policy-failure", f"{type(exc).__name__}: {exc}")
|
|
292
|
+
policy = ToolReport("sarj-library-policy", Completion.FAILED, issues=(issue,))
|
|
293
|
+
native = report_from_tools(self.root, (*native.tools, policy))
|
|
294
|
+
coverage: list[CoverageNotice] = []
|
|
295
|
+
excluded = sum(Path(item).is_file() and item not in active_selected for item in selected)
|
|
296
|
+
if excluded:
|
|
297
|
+
coverage.append(
|
|
298
|
+
CoverageNotice(
|
|
299
|
+
"sarj-standards",
|
|
300
|
+
"excluded by repository policy",
|
|
301
|
+
excluded,
|
|
302
|
+
CoverageDisposition.EXCLUDED,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
routed = _routed_for_selection(selected_groups, rule_selection)
|
|
306
|
+
if adopted is not None and adopted.profile == "application":
|
|
307
|
+
routed.update(
|
|
308
|
+
item
|
|
309
|
+
for item in active_selected
|
|
310
|
+
if Path(item).is_file() and library_policy_accepts_path(Path(item), self.root)
|
|
311
|
+
)
|
|
312
|
+
unsupported = sum(Path(item).is_file() and item not in routed for item in active_selected)
|
|
313
|
+
if unsupported:
|
|
314
|
+
coverage.append(
|
|
315
|
+
CoverageNotice(
|
|
316
|
+
"sarj-standards",
|
|
317
|
+
"no bundled analyzer accepts the selected file type",
|
|
318
|
+
unsupported,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
if not external:
|
|
322
|
+
if selected_groups.typescript and (rule_selection is None or RuleEngine.ESLINT in rule_selection.engines):
|
|
323
|
+
eslint_enabled = adopted is None or "eslint" in adopted.configs
|
|
324
|
+
coverage.append(
|
|
325
|
+
CoverageNotice(
|
|
326
|
+
"eslint",
|
|
327
|
+
(
|
|
328
|
+
"native analysis does not run TypeScript; use check or external trusted analysis"
|
|
329
|
+
if eslint_enabled
|
|
330
|
+
else "disabled by repository capabilities"
|
|
331
|
+
),
|
|
332
|
+
len(selected_groups.typescript),
|
|
333
|
+
CoverageDisposition.FAILED if eslint_enabled else CoverageDisposition.NOT_REQUESTED,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
if normalized_mode in {AnalysisMode.POLICY, AnalysisMode.OBSERVE}:
|
|
337
|
+
native = _with_warning_severity(native, _warning_rule_keys())
|
|
338
|
+
return _with_coverage(_without_baselined_diagnostics(native, baseline_counts), coverage)
|
|
339
|
+
if selected_groups.typescript and adopted is not None and "eslint" not in adopted.configs:
|
|
340
|
+
coverage.append(
|
|
341
|
+
CoverageNotice(
|
|
342
|
+
"eslint",
|
|
343
|
+
"disabled by repository capabilities",
|
|
344
|
+
len(selected_groups.typescript),
|
|
345
|
+
CoverageDisposition.NOT_REQUESTED,
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
run_eslint = rule_selection is None or RuleEngine.ESLINT in rule_selection.engines
|
|
349
|
+
external_reports = (
|
|
350
|
+
(
|
|
351
|
+
analyze_external(
|
|
352
|
+
active_selected,
|
|
353
|
+
root=self.root,
|
|
354
|
+
trust=normalized_trust,
|
|
355
|
+
policy=selection_policy,
|
|
356
|
+
capabilities=(frozenset({"eslint"}) if rule_selection is not None else frozenset(adopted.configs)),
|
|
357
|
+
grouped=selected_groups,
|
|
358
|
+
include_react_doctor=(paths is None or staged) and rule_selection is None,
|
|
359
|
+
react_doctor_staged=staged,
|
|
360
|
+
)
|
|
361
|
+
if adopted is not None
|
|
362
|
+
else analyze_external(
|
|
363
|
+
active_selected,
|
|
364
|
+
root=self.root,
|
|
365
|
+
trust=normalized_trust,
|
|
366
|
+
grouped=selected_groups,
|
|
367
|
+
include_react_doctor=(paths is None or staged) and rule_selection is None,
|
|
368
|
+
react_doctor_staged=staged,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
if run_eslint
|
|
372
|
+
else ()
|
|
373
|
+
)
|
|
374
|
+
if rule_selection is not None:
|
|
375
|
+
external_reports = tuple(_filter_report_selectors(report, rule_selection) for report in external_reports)
|
|
376
|
+
if (
|
|
377
|
+
selected_groups.typescript
|
|
378
|
+
and run_eslint
|
|
379
|
+
and (adopted is None or "eslint" in adopted.configs)
|
|
380
|
+
and not any(report.name == "eslint" for report in external_reports)
|
|
381
|
+
):
|
|
382
|
+
issue = ExecutionIssue(
|
|
383
|
+
"eslint", "coverage-missing", "no ESLint project accepted the selected TypeScript files"
|
|
384
|
+
)
|
|
385
|
+
external_reports = (*external_reports, ToolReport("eslint", Completion.FAILED, issues=(issue,)))
|
|
386
|
+
combined = report_from_tools(self.root, (*native.tools, *external_reports))
|
|
387
|
+
if normalized_mode in {AnalysisMode.POLICY, AnalysisMode.OBSERVE}:
|
|
388
|
+
combined = _with_warning_severity(combined, _warning_rule_keys())
|
|
389
|
+
return _with_coverage(_without_baselined_diagnostics(combined, baseline_counts), coverage)
|
|
390
|
+
|
|
391
|
+
def run(
|
|
392
|
+
self,
|
|
393
|
+
paths: Sequence[str] | None = None,
|
|
394
|
+
*,
|
|
395
|
+
external: bool = False,
|
|
396
|
+
trust: TrustMode | str = TrustMode.SAFE,
|
|
397
|
+
mode: AnalysisMode | str = AnalysisMode.POLICY,
|
|
398
|
+
rules: Sequence[str | RuleSelector] | None = None,
|
|
399
|
+
staged: bool = False,
|
|
400
|
+
) -> AnalysisReport:
|
|
401
|
+
return self.analyze(paths, external=external, trust=trust, mode=mode, rules=rules, staged=staged)
|
|
402
|
+
|
|
403
|
+
def fix(self) -> Result:
|
|
404
|
+
from .libs.adoption import ( # ruff: ignore[import-outside-top-level] -- selected operation only
|
|
405
|
+
lifecycle,
|
|
406
|
+
scaffold,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
adopted = load_manifest(self.root)
|
|
411
|
+
ecosystems = scaffold.detect(self.root) if adopted is None else scaffold.detect_adopted(self.root, adopted)
|
|
412
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
413
|
+
return Result(
|
|
414
|
+
Status.INVALID,
|
|
415
|
+
findings=(Finding("fix.input.invalid", "error", str(exc)),),
|
|
416
|
+
exit_code=_INVALID_EXIT,
|
|
417
|
+
)
|
|
418
|
+
return _operation_result(lifecycle.execute(lifecycle.format_commands(ecosystems)))
|
|
419
|
+
|
|
420
|
+
def doctor(self) -> Result:
|
|
421
|
+
diagnosed = diagnose(self.root)
|
|
422
|
+
findings = tuple(
|
|
423
|
+
Finding(item.id, item.level.value, item.detail, item.where, item.remediation) for item in diagnosed
|
|
424
|
+
)
|
|
425
|
+
status = _doctor_status(diagnosed)
|
|
426
|
+
return _operation_result(status, findings=findings)
|
|
427
|
+
|
|
428
|
+
def update(
|
|
429
|
+
self,
|
|
430
|
+
*,
|
|
431
|
+
version: str | None = None,
|
|
432
|
+
offline: bool = False,
|
|
433
|
+
install: bool = True,
|
|
434
|
+
check_only: bool = False,
|
|
435
|
+
) -> Result:
|
|
436
|
+
return self._update_target(version=version, offline=offline, install=install, check_only=check_only)
|
|
437
|
+
|
|
438
|
+
def _update_target(self, *, version: str | None, offline: bool, install: bool, check_only: bool) -> Result:
|
|
439
|
+
executable = shutil.which("uvx")
|
|
440
|
+
if executable is None:
|
|
441
|
+
finding = Finding(
|
|
442
|
+
"update.latest.unavailable",
|
|
443
|
+
"error",
|
|
444
|
+
"uvx is required to resolve the latest Standards release; install uv and retry",
|
|
445
|
+
)
|
|
446
|
+
return Result(Status.FAILED, findings=(finding,), exit_code=_INVALID_EXIT)
|
|
447
|
+
try:
|
|
448
|
+
launch_argv = launcher.argv(executable=executable, version=version, refresh=not offline)
|
|
449
|
+
except ValueError as exc:
|
|
450
|
+
finding = Finding("update.version.invalid", "error", str(exc))
|
|
451
|
+
return Result(Status.INVALID, findings=(finding,), exit_code=_INVALID_EXIT)
|
|
452
|
+
command = [
|
|
453
|
+
*launch_argv,
|
|
454
|
+
"--root",
|
|
455
|
+
str(self.root),
|
|
456
|
+
"update",
|
|
457
|
+
"--offline",
|
|
458
|
+
]
|
|
459
|
+
if version is not None:
|
|
460
|
+
command.extend(("--to", version))
|
|
461
|
+
if check_only:
|
|
462
|
+
command.append("--check")
|
|
463
|
+
if not install:
|
|
464
|
+
command.append("--no-install")
|
|
465
|
+
environment = dict(os.environ) # ruff: ignore[banned-api] -- preserve caller environment for the fixed uvx process.
|
|
466
|
+
environment["SARJ_STANDARDS_BOOTSTRAPPED"] = "1"
|
|
467
|
+
try:
|
|
468
|
+
completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- fixed executable and argv.
|
|
469
|
+
command,
|
|
470
|
+
check=False,
|
|
471
|
+
capture_output=True,
|
|
472
|
+
text=True,
|
|
473
|
+
env=environment,
|
|
474
|
+
)
|
|
475
|
+
except OSError as exc:
|
|
476
|
+
finding = Finding("update.latest.failed", "error", str(exc))
|
|
477
|
+
return Result(Status.FAILED, findings=(finding,), exit_code=_INVALID_EXIT)
|
|
478
|
+
output = (completed.stderr or completed.stdout).strip()
|
|
479
|
+
if completed.returncode == 0:
|
|
480
|
+
status = Status.OK if check_only else Status.CHANGED
|
|
481
|
+
changes = () if check_only else (Change("update", "applied the latest coherent Standards bundle"),)
|
|
482
|
+
return Result(status, changes=changes)
|
|
483
|
+
if completed.returncode == 1:
|
|
484
|
+
if check_only:
|
|
485
|
+
finding = Finding("update.latest.available", "warning", output or "a standards update is available")
|
|
486
|
+
return Result(Status.DRIFT, findings=(finding,), exit_code=1)
|
|
487
|
+
finding = Finding(
|
|
488
|
+
"update.latest.failed",
|
|
489
|
+
"error",
|
|
490
|
+
output or "the latest standards update did not converge; tracked configuration files were restored",
|
|
491
|
+
)
|
|
492
|
+
return Result(Status.FAILED, findings=(finding,), exit_code=1)
|
|
493
|
+
finding = Finding("update.latest.failed", "error", output or "latest standards update failed")
|
|
494
|
+
return Result(Status.FAILED, findings=(finding,), exit_code=completed.returncode)
|
|
495
|
+
|
|
496
|
+
def inspect(self) -> Inspection:
|
|
497
|
+
return inspect(self.root)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _doctor_status(findings: Sequence[DoctorFinding]) -> int:
|
|
501
|
+
if any(finding.id.endswith(".invalid") or finding.id in _INVALID_DOCTOR_FINDING_IDS for finding in findings):
|
|
502
|
+
return _INVALID_EXIT
|
|
503
|
+
return (
|
|
504
|
+
1
|
|
505
|
+
if any(finding.level is DoctorLevel.DRIFT or finding.id == "doctor.manifest.absent" for finding in findings)
|
|
506
|
+
else 0
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _doctor_findings(findings: Sequence[DoctorFinding]) -> tuple[Finding, ...]:
|
|
511
|
+
return tuple(Finding(item.id, item.level.value, item.detail, item.where, item.remediation) for item in findings)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _verify(root: Path) -> int:
|
|
515
|
+
status = _doctor_status(diagnose(root))
|
|
516
|
+
if status:
|
|
517
|
+
return status
|
|
518
|
+
sync_result = apply_sync(plan_sync(root), check=True)
|
|
519
|
+
if sync_result.status:
|
|
520
|
+
return sync_result.status
|
|
521
|
+
try:
|
|
522
|
+
adopted = load_manifest(root)
|
|
523
|
+
except OSError, TypeError, ValueError:
|
|
524
|
+
return _INVALID_EXIT
|
|
525
|
+
verify_paths = adopted.verify_paths if adopted is not None else (".",)
|
|
526
|
+
policy = Policy.from_manifest(root, adopted)
|
|
527
|
+
if check(
|
|
528
|
+
[str(root / path) for path in verify_paths],
|
|
529
|
+
policy=policy,
|
|
530
|
+
):
|
|
531
|
+
return 1
|
|
532
|
+
if adopted is not None and adopted.profile == "application" and check_library_policy(root):
|
|
533
|
+
return 1
|
|
534
|
+
return execute(verification_commands(detect(root)))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _init_changes(plan: InitPlan, *, install: bool) -> tuple[Change, ...]:
|
|
538
|
+
scaffold = plan.scaffold
|
|
539
|
+
return (
|
|
540
|
+
*(Change("create", "write adoption file", path) for path, _contents in scaffold.writes),
|
|
541
|
+
*(Change("update", "extend adoption file", path) for path, _contents in scaffold.edits),
|
|
542
|
+
*(Change("delete", "remove retired repository launcher", path) for path in scaffold.deletes),
|
|
543
|
+
*(
|
|
544
|
+
Change(
|
|
545
|
+
"create" if not target.destination.exists() else "update",
|
|
546
|
+
f"sync {target.name} config",
|
|
547
|
+
target.destination,
|
|
548
|
+
)
|
|
549
|
+
for target in (() if plan.sync is None else plan.sync.targets)
|
|
550
|
+
if _sync_target_changes(target.source, target.destination)
|
|
551
|
+
),
|
|
552
|
+
*(Change("run", command.label) for command in plan.install_commands if install),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _operation_result(
|
|
557
|
+
exit_code: int,
|
|
558
|
+
changes: tuple[Change, ...] = (),
|
|
559
|
+
*,
|
|
560
|
+
findings: tuple[Finding, ...] = (),
|
|
561
|
+
) -> Result:
|
|
562
|
+
if exit_code == 0:
|
|
563
|
+
status = Status.CHANGED if changes else Status.OK
|
|
564
|
+
elif exit_code == 1:
|
|
565
|
+
status = Status.DRIFT
|
|
566
|
+
elif exit_code == _INVALID_EXIT:
|
|
567
|
+
status = Status.INVALID
|
|
568
|
+
elif exit_code == _INTERRUPTED_EXIT:
|
|
569
|
+
status = Status.INTERRUPTED
|
|
570
|
+
else:
|
|
571
|
+
status = Status.FAILED
|
|
572
|
+
return Result(status, findings, changes, exit_code)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _failed_analysis(root: Path, kind: str, message: str) -> AnalysisReport:
|
|
576
|
+
issue = ExecutionIssue("sarj-standards", kind, message)
|
|
577
|
+
tool = ToolReport("sarj-standards", Completion.FAILED, issues=(issue,))
|
|
578
|
+
return AnalysisReport(root, Completion.FAILED, Conclusion.INCONCLUSIVE, (tool,))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _without_baselined_diagnostics(report: AnalysisReport, counts: dict[str, int]) -> AnalysisReport:
|
|
582
|
+
if not counts:
|
|
583
|
+
return report
|
|
584
|
+
remaining = counts.copy()
|
|
585
|
+
|
|
586
|
+
def active(diagnostic: Diagnostic) -> bool:
|
|
587
|
+
if not diagnostic_baseline.is_baselineable(diagnostic) or diagnostic.code == "SARJ206":
|
|
588
|
+
return True
|
|
589
|
+
fingerprint = diagnostic.fingerprint
|
|
590
|
+
budget = 0 if fingerprint is None else remaining.get(fingerprint, 0)
|
|
591
|
+
if budget < 1 or fingerprint is None:
|
|
592
|
+
return True
|
|
593
|
+
remaining[fingerprint] = budget - 1
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
tools = tuple(
|
|
597
|
+
ToolReport(
|
|
598
|
+
item.name,
|
|
599
|
+
item.completion,
|
|
600
|
+
diagnostics=tuple(diagnostic for diagnostic in item.diagnostics if active(diagnostic)),
|
|
601
|
+
issues=item.issues,
|
|
602
|
+
analyzer_id=item.analyzer_id,
|
|
603
|
+
invocation_id=item.invocation_id,
|
|
604
|
+
version=item.version,
|
|
605
|
+
duration_ms=item.duration_ms,
|
|
606
|
+
file_count=item.file_count,
|
|
607
|
+
cache_status=item.cache_status,
|
|
608
|
+
)
|
|
609
|
+
for item in report.tools
|
|
610
|
+
)
|
|
611
|
+
return report_from_tools(report.root, tools)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _with_coverage(report: AnalysisReport, coverage: Sequence[CoverageNotice]) -> AnalysisReport:
|
|
615
|
+
notices = tuple(coverage)
|
|
616
|
+
if not notices:
|
|
617
|
+
return report
|
|
618
|
+
blocking = any(item.blocking for item in notices)
|
|
619
|
+
completion = Completion.PARTIAL if blocking and report.completion is Completion.COMPLETE else report.completion
|
|
620
|
+
conclusion = report.conclusion if report.diagnostics or not blocking else Conclusion.INCONCLUSIVE
|
|
621
|
+
return AnalysisReport(report.root, completion, conclusion, report.tools, notices)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _analysis_inputs(root: Path, paths: Sequence[str] | None, *, mode: AnalysisMode = AnalysisMode.POLICY) -> list[str]:
|
|
625
|
+
if paths is not None:
|
|
626
|
+
selected = _contained_paths(root, paths)
|
|
627
|
+
return selected if mode is AnalysisMode.RAW else _with_tracked_terraform_tests(root, selected)
|
|
628
|
+
if mode is AnalysisMode.RAW:
|
|
629
|
+
return [str(root)]
|
|
630
|
+
adopted = load_manifest(root)
|
|
631
|
+
verify_paths = adopted.verify_paths if adopted is not None else (".",)
|
|
632
|
+
return _with_tracked_terraform_tests(root, [str(root / path) for path in verify_paths])
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _with_tracked_terraform_tests(root: Path, selected: list[str]) -> list[str]:
|
|
636
|
+
git = shutil.which("git")
|
|
637
|
+
if git is None:
|
|
638
|
+
return selected
|
|
639
|
+
completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- fixed git argv.
|
|
640
|
+
(git, "-C", str(root), "ls-files", "-z"),
|
|
641
|
+
check=False,
|
|
642
|
+
capture_output=True,
|
|
643
|
+
)
|
|
644
|
+
if completed.returncode != 0:
|
|
645
|
+
return selected
|
|
646
|
+
tracked = [
|
|
647
|
+
str(root / item)
|
|
648
|
+
for item in completed.stdout.decode("utf-8", errors="replace").split("\0")
|
|
649
|
+
if item.casefold().endswith((".tftest.hcl", ".tftest.json"))
|
|
650
|
+
]
|
|
651
|
+
return list(dict.fromkeys((*selected, *tracked)))
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _rule_selection(values: Sequence[str | RuleSelector] | None) -> RuleSelection | None:
|
|
655
|
+
if values is None:
|
|
656
|
+
return None
|
|
657
|
+
if isinstance(values, str):
|
|
658
|
+
msg = "rules must be a sequence of canonical selectors, not one string"
|
|
659
|
+
raise TypeError(msg)
|
|
660
|
+
from sarj_standards.libs.repository import rule_catalog_artifact # ruff: ignore[import-outside-top-level]
|
|
661
|
+
|
|
662
|
+
catalog = rule_catalog_artifact.load()
|
|
663
|
+
raw_rules = _object_list(catalog.get("rules"), "shipped rule catalog rules")
|
|
664
|
+
live: set[RuleSelector] = set()
|
|
665
|
+
for value in raw_rules:
|
|
666
|
+
key: object = value.get("key") if isinstance(value, dict) else None # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
|
|
667
|
+
if isinstance(key, str):
|
|
668
|
+
live.add(RuleSelector.parse(key))
|
|
669
|
+
selected: set[RuleSelector] = set()
|
|
670
|
+
for value in values:
|
|
671
|
+
selector = value if isinstance(value, RuleSelector) else RuleSelector.parse(value)
|
|
672
|
+
if selector not in live:
|
|
673
|
+
msg = f"unknown or invalid rule selector: {value}"
|
|
674
|
+
raise ValueError(msg)
|
|
675
|
+
selected.add(selector)
|
|
676
|
+
return RuleSelection(frozenset(selected))
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _routed_for_selection(grouped: object, selected: RuleSelection | None) -> set[str]:
|
|
680
|
+
from sarj_standards.libs.linting.runner import GroupedPaths # ruff: ignore[import-outside-top-level]
|
|
681
|
+
|
|
682
|
+
if not isinstance(grouped, GroupedPaths):
|
|
683
|
+
msg = "analysis routing has an invalid internal type"
|
|
684
|
+
raise TypeError(msg)
|
|
685
|
+
engines = frozenset(RuleEngine) if selected is None else selected.engines
|
|
686
|
+
routed: set[str] = set()
|
|
687
|
+
if RuleEngine.PYTHON in engines:
|
|
688
|
+
routed.update(grouped.python)
|
|
689
|
+
if RuleEngine.SQL in engines:
|
|
690
|
+
routed.update(grouped.sql)
|
|
691
|
+
if RuleEngine.IAC in engines:
|
|
692
|
+
routed.update(grouped.iac)
|
|
693
|
+
if RuleEngine.TEXT in engines:
|
|
694
|
+
routed.update(grouped.text)
|
|
695
|
+
if RuleEngine.ESLINT in engines:
|
|
696
|
+
routed.update(grouped.typescript)
|
|
697
|
+
return routed
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _filter_report_selectors(
|
|
701
|
+
report: ToolReport,
|
|
702
|
+
selected: RuleSelection,
|
|
703
|
+
) -> ToolReport:
|
|
704
|
+
engine = RuleEngine.ESLINT if report.name == "eslint" else None
|
|
705
|
+
allowed: frozenset[str] = frozenset() if engine is None else selected.native_ids_for(engine)
|
|
706
|
+
return ToolReport(
|
|
707
|
+
report.name,
|
|
708
|
+
report.completion,
|
|
709
|
+
diagnostics=tuple(item for item in report.diagnostics if item.rule_id in allowed),
|
|
710
|
+
issues=report.issues,
|
|
711
|
+
analyzer_id=report.analyzer_id,
|
|
712
|
+
invocation_id=report.invocation_id,
|
|
713
|
+
version=report.version,
|
|
714
|
+
duration_ms=report.duration_ms,
|
|
715
|
+
file_count=report.file_count,
|
|
716
|
+
cache_status=report.cache_status,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _warning_rule_keys() -> frozenset[RuleSelector]:
|
|
721
|
+
from sarj_standards.libs.linting.policy import ( # ruff: ignore[import-outside-top-level]
|
|
722
|
+
warning_selectors,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
return warning_selectors()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _with_warning_severity(report: AnalysisReport, selectors: frozenset[RuleSelector]) -> AnalysisReport:
|
|
729
|
+
if not selectors:
|
|
730
|
+
return report
|
|
731
|
+
tools = tuple(
|
|
732
|
+
ToolReport(
|
|
733
|
+
tool.name,
|
|
734
|
+
tool.completion,
|
|
735
|
+
diagnostics=tuple(
|
|
736
|
+
replace(item, severity=Severity.WARNING) if _selector_for_diagnostic(item) in selectors else item
|
|
737
|
+
for item in tool.diagnostics
|
|
738
|
+
),
|
|
739
|
+
issues=tool.issues,
|
|
740
|
+
analyzer_id=tool.analyzer_id,
|
|
741
|
+
invocation_id=tool.invocation_id,
|
|
742
|
+
version=tool.version,
|
|
743
|
+
duration_ms=tool.duration_ms,
|
|
744
|
+
file_count=tool.file_count,
|
|
745
|
+
cache_status=tool.cache_status,
|
|
746
|
+
)
|
|
747
|
+
for tool in report.tools
|
|
748
|
+
)
|
|
749
|
+
return report_from_tools(report.root, tools)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _selector_for_diagnostic(item: Diagnostic) -> RuleSelector | None:
|
|
753
|
+
engine = _engine_for_diagnostic(item)
|
|
754
|
+
if engine is None:
|
|
755
|
+
return None
|
|
756
|
+
identity = item.rule_id or item.code
|
|
757
|
+
if engine is RuleEngine.ESLINT and identity.startswith("@sarj/"):
|
|
758
|
+
identity = identity.removeprefix("@sarj/")
|
|
759
|
+
try:
|
|
760
|
+
return RuleSelector(engine, RuleId(identity))
|
|
761
|
+
except ValueError:
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _engine_for_diagnostic(item: Diagnostic) -> RuleEngine | None:
|
|
766
|
+
return {
|
|
767
|
+
"sarj-python-lint": RuleEngine.PYTHON,
|
|
768
|
+
"sarj-sql-lint": RuleEngine.SQL,
|
|
769
|
+
"sarj-iac-lint": RuleEngine.IAC,
|
|
770
|
+
"sarj-text-lint": RuleEngine.TEXT,
|
|
771
|
+
"python": RuleEngine.PYTHON,
|
|
772
|
+
"sql": RuleEngine.SQL,
|
|
773
|
+
"iac": RuleEngine.IAC,
|
|
774
|
+
"text": RuleEngine.TEXT,
|
|
775
|
+
"eslint": RuleEngine.ESLINT,
|
|
776
|
+
}.get(item.source)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _policy_report(root: Path, selected: Sequence[str]) -> ToolReport:
|
|
780
|
+
findings = check_selected_library_policy(root, selected)
|
|
781
|
+
documents: dict[Path, SourceDocument] = {}
|
|
782
|
+
diagnostics: list[Diagnostic] = []
|
|
783
|
+
for finding in findings:
|
|
784
|
+
resolved = (root / finding.path).resolve()
|
|
785
|
+
relative = resolved.relative_to(root)
|
|
786
|
+
if resolved not in documents:
|
|
787
|
+
documents[resolved] = SourceDocument.read(resolved)
|
|
788
|
+
position = documents[resolved].point(line=finding.line, column=finding.column)
|
|
789
|
+
diagnostics.append(
|
|
790
|
+
Diagnostic(
|
|
791
|
+
finding.id,
|
|
792
|
+
finding.message,
|
|
793
|
+
Severity.ERROR,
|
|
794
|
+
"sarj-library-policy",
|
|
795
|
+
Location(relative.as_posix(), position=position),
|
|
796
|
+
rule_id=finding.id,
|
|
797
|
+
help=f"Replace {finding.package} with {finding.replacement}",
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
return ToolReport("sarj-library-policy", Completion.COMPLETE, diagnostics=tuple(diagnostics))
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _filter_tool_report(report: ToolReport, policy: Policy) -> ToolReport:
|
|
804
|
+
return ToolReport(
|
|
805
|
+
report.name,
|
|
806
|
+
report.completion,
|
|
807
|
+
diagnostics=policy.filter_diagnostics(report.diagnostics),
|
|
808
|
+
issues=report.issues,
|
|
809
|
+
analyzer_id=report.analyzer_id,
|
|
810
|
+
invocation_id=report.invocation_id,
|
|
811
|
+
version=report.version,
|
|
812
|
+
duration_ms=report.duration_ms,
|
|
813
|
+
file_count=report.file_count,
|
|
814
|
+
cache_status=report.cache_status,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _repository_root(root: str | Path) -> Path:
|
|
819
|
+
resolved = Path(root).resolve()
|
|
820
|
+
if not resolved.is_dir():
|
|
821
|
+
msg = f"repository root {resolved} is not a directory"
|
|
822
|
+
raise ValueError(msg)
|
|
823
|
+
return resolved
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _contained_paths(root: Path, paths: Sequence[str]) -> list[str]:
|
|
827
|
+
selected: list[str] = []
|
|
828
|
+
for raw in paths:
|
|
829
|
+
supplied = Path(raw)
|
|
830
|
+
candidate = supplied if supplied.is_absolute() else root / supplied
|
|
831
|
+
try:
|
|
832
|
+
relative = candidate.absolute().relative_to(root)
|
|
833
|
+
except ValueError as exc:
|
|
834
|
+
msg = f"input must exist inside repository root: {raw}"
|
|
835
|
+
raise ValueError(msg) from exc
|
|
836
|
+
cursor = root
|
|
837
|
+
if any(is_link_like(cursor := cursor / part) for part in relative.parts):
|
|
838
|
+
msg = f"input must not traverse a symlink: {raw}"
|
|
839
|
+
raise ValueError(msg)
|
|
840
|
+
resolved = candidate.resolve()
|
|
841
|
+
if not resolved.is_relative_to(root) or not resolved.exists():
|
|
842
|
+
msg = f"input must exist inside repository root: {raw}"
|
|
843
|
+
raise ValueError(msg)
|
|
844
|
+
selected.append(str(resolved))
|
|
845
|
+
return list(dict.fromkeys(selected))
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _sync_target_changes(source: Path, destination: Path) -> bool:
|
|
849
|
+
try:
|
|
850
|
+
return not destination.is_file() or source.read_bytes() != destination.read_bytes()
|
|
851
|
+
except OSError:
|
|
852
|
+
return True
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _object_list(value: object, label: str) -> list[object]:
|
|
856
|
+
if not isinstance(value, list):
|
|
857
|
+
msg = f"{label} must be an array"
|
|
858
|
+
raise TypeError(msg)
|
|
859
|
+
return value # pyright: ignore[reportUnknownVariableType]
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
__all__ = [
|
|
863
|
+
"AnalysisMode",
|
|
864
|
+
"AnalysisReport",
|
|
865
|
+
"Change",
|
|
866
|
+
"Completion",
|
|
867
|
+
"Conclusion",
|
|
868
|
+
"CoverageNotice",
|
|
869
|
+
"Diagnostic",
|
|
870
|
+
"ExecutionIssue",
|
|
871
|
+
"Finding",
|
|
872
|
+
"Fix",
|
|
873
|
+
"FixSafety",
|
|
874
|
+
"Location",
|
|
875
|
+
"Position",
|
|
876
|
+
"Region",
|
|
877
|
+
"RelatedLocation",
|
|
878
|
+
"Result",
|
|
879
|
+
"Severity",
|
|
880
|
+
"Standards",
|
|
881
|
+
"Status",
|
|
882
|
+
"TextEdit",
|
|
883
|
+
"ToolReport",
|
|
884
|
+
"TrustMode",
|
|
885
|
+
"__version__",
|
|
886
|
+
"to_github",
|
|
887
|
+
"to_json",
|
|
888
|
+
"to_sarif",
|
|
889
|
+
"to_text",
|
|
890
|
+
]
|