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,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
import json
|
|
7
|
+
from operator import itemgetter
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, Final, NamedTuple, Protocol
|
|
10
|
+
|
|
11
|
+
from sarj_standards.libs.adoption.manifest import as_table, list_field
|
|
12
|
+
from sarj_standards.libs.linting import textlint
|
|
13
|
+
|
|
14
|
+
from . import ledger, repository
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_FAMILIES: Final = (
|
|
22
|
+
("python", "sarj_python_lint.rules", "packages/python"),
|
|
23
|
+
("sql", "sarj_sql_lint.rules", "packages/sql"),
|
|
24
|
+
("iac", "sarj_iac_lint.rules", "packages/iac"),
|
|
25
|
+
)
|
|
26
|
+
_RENAME_ENTRY: Final = re.compile(r'^\s*"(?P<old>[a-z0-9-]+)": "(?P<new>[a-z0-9-]+)",', re.MULTILINE)
|
|
27
|
+
_PLACEHOLDER: Final = "TODO: say why it went, in one line a consumer can act on"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Rule(Protocol):
|
|
31
|
+
code: str
|
|
32
|
+
__module__: str
|
|
33
|
+
documentation: RuleDocumentation | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RuleDocumentation(Protocol):
|
|
37
|
+
aliases: tuple[str, ...]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _NativeLedgerState(NamedTuple):
|
|
41
|
+
rules: dict[str, list[str]]
|
|
42
|
+
codes: dict[str, list[str]]
|
|
43
|
+
renames: dict[tuple[str, str], str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class SyncResult:
|
|
48
|
+
status: int
|
|
49
|
+
message: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def inventory(root: Path) -> list[dict[str, str]]:
|
|
53
|
+
items: list[dict[str, str]] = []
|
|
54
|
+
for family, module_name, package in _FAMILIES:
|
|
55
|
+
registry = _registry(module_name)
|
|
56
|
+
for rule_id, rule in sorted(registry.items()):
|
|
57
|
+
module = rule.__module__.replace(".", "/")
|
|
58
|
+
source_name = module.rsplit("/", 1)[-1]
|
|
59
|
+
items.append(
|
|
60
|
+
{
|
|
61
|
+
"family": family,
|
|
62
|
+
"id": rule_id,
|
|
63
|
+
"code": str(rule.code),
|
|
64
|
+
"source": f"{package}/src/{module}.py",
|
|
65
|
+
"test": f"{package}/tests/rules/test_{source_name}.py",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
for rule_id, meta in sorted(textlint.REGISTRY.items()):
|
|
69
|
+
items.append(
|
|
70
|
+
{
|
|
71
|
+
"family": "text",
|
|
72
|
+
"id": rule_id,
|
|
73
|
+
"code": meta.code,
|
|
74
|
+
"source": "packages/standards/src/sarj_standards/libs/linting/textlint.py",
|
|
75
|
+
"test": "packages/standards/tests/test_textlint.py",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
eslint_rules = (
|
|
79
|
+
repository.eslint_rule_names(root)
|
|
80
|
+
if (root / "packages/typescript/src/index.ts").is_file()
|
|
81
|
+
else list(ledger.load().rules.get(ledger.ESLINT, ()))
|
|
82
|
+
)
|
|
83
|
+
items.extend(
|
|
84
|
+
{
|
|
85
|
+
"family": "typescript",
|
|
86
|
+
"id": rule_id,
|
|
87
|
+
"code": f"@sarj/{rule_id}",
|
|
88
|
+
"source": f"packages/typescript/src/rules/{rule_id}.ts",
|
|
89
|
+
"test": f"packages/typescript/tests/rules/{rule_id}.test.ts",
|
|
90
|
+
}
|
|
91
|
+
for rule_id in eslint_rules
|
|
92
|
+
)
|
|
93
|
+
return sorted(items, key=itemgetter("family", "id"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sync_ledger(root: Path, *, check: bool) -> SyncResult:
|
|
97
|
+
path = root / "packages/standards/src/sarj_standards/configs/rule-ledger.json"
|
|
98
|
+
previous = _load_ledger(path)
|
|
99
|
+
rules, codes, source_renames = _native_ledger_state()
|
|
100
|
+
rules["eslint"] = repository.eslint_rule_names(root)
|
|
101
|
+
rules["text"] = sorted(textlint.REGISTRY)
|
|
102
|
+
codes["text"] = sorted(meta.code for meta in textlint.REGISTRY.values())
|
|
103
|
+
for rule_id, meta in textlint.REGISTRY.items():
|
|
104
|
+
for old_id in meta.aliases:
|
|
105
|
+
source_renames["text", old_id] = rule_id
|
|
106
|
+
retired = _retired(previous, root, source_renames)
|
|
107
|
+
known = {str(entry.get("id")) for entry in retired}
|
|
108
|
+
old_rules = repository_table(previous.get("rules"))
|
|
109
|
+
old_codes = repository_table(previous.get("codes"))
|
|
110
|
+
for family, raw_names in old_rules.items():
|
|
111
|
+
prefix = "@sarj/" if family == "eslint" else ""
|
|
112
|
+
for name in _string_list(raw_names):
|
|
113
|
+
identifier = f"{prefix}{name}"
|
|
114
|
+
if name not in rules.get(family, []) and identifier not in known:
|
|
115
|
+
retired.append(_retired_entry(identifier, family))
|
|
116
|
+
known.add(identifier)
|
|
117
|
+
for family, raw_codes in old_codes.items():
|
|
118
|
+
for code in _string_list(raw_codes):
|
|
119
|
+
if code not in codes.get(family, []) and code not in known:
|
|
120
|
+
retired.append(_retired_entry(code, "code"))
|
|
121
|
+
known.add(code)
|
|
122
|
+
updated = {
|
|
123
|
+
"$comment": previous.get("$comment", "Rule compatibility ledger."),
|
|
124
|
+
"rules": rules,
|
|
125
|
+
"codes": codes,
|
|
126
|
+
"retired": sorted(retired, key=lambda entry: str(entry.get("id"))),
|
|
127
|
+
}
|
|
128
|
+
rendered = json.dumps(updated, indent=2) + "\n"
|
|
129
|
+
current = path.read_text(encoding="utf-8")
|
|
130
|
+
if rendered == current:
|
|
131
|
+
return SyncResult(0, f"ok: {path.name} matches the registries")
|
|
132
|
+
if check:
|
|
133
|
+
return SyncResult(1, f"drift: {path}")
|
|
134
|
+
path.write_text(rendered, encoding="utf-8")
|
|
135
|
+
status = 1 if any(entry.get("note") == _PLACEHOLDER for entry in retired) else 0
|
|
136
|
+
return SyncResult(status, f"wrote: {path}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _native_ledger_state() -> _NativeLedgerState:
|
|
140
|
+
rules: dict[str, list[str]] = {}
|
|
141
|
+
codes: dict[str, list[str]] = {}
|
|
142
|
+
renames: dict[tuple[str, str], str] = {}
|
|
143
|
+
for family, module_name, _package in _FAMILIES:
|
|
144
|
+
registry = _registry(module_name)
|
|
145
|
+
rules[family] = sorted(registry)
|
|
146
|
+
codes[family] = sorted(str(rule.code) for rule in registry.values())
|
|
147
|
+
for rule_id, rule in registry.items():
|
|
148
|
+
if rule.documentation is None:
|
|
149
|
+
continue
|
|
150
|
+
for old_id in rule.documentation.aliases:
|
|
151
|
+
renames[family, old_id] = rule_id
|
|
152
|
+
return _NativeLedgerState(rules, codes, renames)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def repository_table(value: object) -> dict[str, object]:
|
|
156
|
+
return as_table(value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _load_ledger(path: Path) -> dict[str, object]:
|
|
160
|
+
value: object = json.loads(path.read_text(encoding="utf-8")) # pyright: ignore[reportAny]
|
|
161
|
+
return repository_table(value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _registry(module_name: str) -> Mapping[str, type[Rule]]:
|
|
165
|
+
module = import_module(module_name)
|
|
166
|
+
value = getattr(module, "REGISTRY", None)
|
|
167
|
+
if not isinstance(value, Mapping):
|
|
168
|
+
msg = f"{module_name} has no registry"
|
|
169
|
+
raise TypeError(msg)
|
|
170
|
+
return value # pyright: ignore[reportUnknownVariableType]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _retired(
|
|
174
|
+
previous: Mapping[str, object],
|
|
175
|
+
root: Path,
|
|
176
|
+
source_renames: Mapping[tuple[str, str], str],
|
|
177
|
+
) -> list[dict[str, object]]:
|
|
178
|
+
entries = [repository_table(item) for item in _object_list(previous.get("retired"))]
|
|
179
|
+
derived_kinds = {"eslint", *(kind for kind, _old_id in source_renames)}
|
|
180
|
+
kept = [entry for entry in entries if not (entry.get("kind") in derived_kinds and entry.get("status") == "renamed")]
|
|
181
|
+
existing = {str(entry.get("id")): entry for entry in entries}
|
|
182
|
+
for (kind, old), new in sorted(source_renames.items()):
|
|
183
|
+
prior = existing.get(old)
|
|
184
|
+
kept.append(
|
|
185
|
+
prior
|
|
186
|
+
if prior is not None and prior.get("replacement") == new
|
|
187
|
+
else {
|
|
188
|
+
"id": old,
|
|
189
|
+
"kind": kind,
|
|
190
|
+
"status": "renamed",
|
|
191
|
+
"replacement": new,
|
|
192
|
+
"note": f"Replace sarj-{old} with sarj-{new} before upgrading.",
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
renames_path = root / "packages/typescript/src/rules/_renames.ts"
|
|
196
|
+
for match in _RENAME_ENTRY.finditer(renames_path.read_text(encoding="utf-8")):
|
|
197
|
+
old = match.group("old")
|
|
198
|
+
new = match.group("new")
|
|
199
|
+
identifier = f"@sarj/{old}"
|
|
200
|
+
replacement = f"@sarj/{new}"
|
|
201
|
+
prior = existing.get(identifier)
|
|
202
|
+
kept.append(
|
|
203
|
+
prior
|
|
204
|
+
if prior is not None and prior.get("replacement") == replacement
|
|
205
|
+
else {
|
|
206
|
+
"id": identifier,
|
|
207
|
+
"kind": "eslint",
|
|
208
|
+
"status": "renamed",
|
|
209
|
+
"replacement": replacement,
|
|
210
|
+
"note": f"Replace @sarj/{old} with @sarj/{new} before upgrading.",
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
return kept
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _retired_entry(identifier: str, kind: str) -> dict[str, object]:
|
|
217
|
+
return {"id": identifier, "kind": kind, "status": "removed", "replacement": None, "note": _PLACEHOLDER}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _object_list(value: object) -> list[object]:
|
|
221
|
+
return list_field({"value": value}, "value")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _string_list(value: object) -> list[str]:
|
|
225
|
+
return [item for item in _object_list(value) if isinstance(item, str)]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from .catalog import DocumentedRule, RuleCatalog, RuleCatalogDocument
|
|
2
|
+
from .contracts import (
|
|
3
|
+
AutofixPolicy,
|
|
4
|
+
CatalogRule,
|
|
5
|
+
DefaultLevel,
|
|
6
|
+
EvaluationCase,
|
|
7
|
+
ExampleFile,
|
|
8
|
+
ExpectedOutcome,
|
|
9
|
+
Finding,
|
|
10
|
+
Language,
|
|
11
|
+
MessageId,
|
|
12
|
+
RuleCategory,
|
|
13
|
+
RuleEngine,
|
|
14
|
+
RuleExample,
|
|
15
|
+
RuleId,
|
|
16
|
+
RuleOrigin,
|
|
17
|
+
RuleProblem,
|
|
18
|
+
RuleSelection,
|
|
19
|
+
RuleSelector,
|
|
20
|
+
RuleSpec,
|
|
21
|
+
RuleStatus,
|
|
22
|
+
)
|
|
23
|
+
from .corpus_runner import (
|
|
24
|
+
CorpusBatchResult,
|
|
25
|
+
CorpusLintError,
|
|
26
|
+
IsolatedCorpusReport,
|
|
27
|
+
run_isolated_corpora,
|
|
28
|
+
)
|
|
29
|
+
from .evaluation import (
|
|
30
|
+
CaseEvaluation,
|
|
31
|
+
EvaluationEvidence,
|
|
32
|
+
EvaluationReport,
|
|
33
|
+
EvaluationThresholds,
|
|
34
|
+
PromotionDecision,
|
|
35
|
+
RuleEvaluator,
|
|
36
|
+
evaluate,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AutofixPolicy",
|
|
42
|
+
"CaseEvaluation",
|
|
43
|
+
"CatalogRule",
|
|
44
|
+
"CorpusBatchResult",
|
|
45
|
+
"CorpusLintError",
|
|
46
|
+
"DefaultLevel",
|
|
47
|
+
"DocumentedRule",
|
|
48
|
+
"EvaluationCase",
|
|
49
|
+
"EvaluationEvidence",
|
|
50
|
+
"EvaluationReport",
|
|
51
|
+
"EvaluationThresholds",
|
|
52
|
+
"ExampleFile",
|
|
53
|
+
"ExpectedOutcome",
|
|
54
|
+
"Finding",
|
|
55
|
+
"IsolatedCorpusReport",
|
|
56
|
+
"Language",
|
|
57
|
+
"MessageId",
|
|
58
|
+
"PromotionDecision",
|
|
59
|
+
"RuleCatalog",
|
|
60
|
+
"RuleCatalogDocument",
|
|
61
|
+
"RuleCategory",
|
|
62
|
+
"RuleEngine",
|
|
63
|
+
"RuleEvaluator",
|
|
64
|
+
"RuleExample",
|
|
65
|
+
"RuleId",
|
|
66
|
+
"RuleOrigin",
|
|
67
|
+
"RuleProblem",
|
|
68
|
+
"RuleSelection",
|
|
69
|
+
"RuleSelector",
|
|
70
|
+
"RuleSpec",
|
|
71
|
+
"RuleStatus",
|
|
72
|
+
"evaluate",
|
|
73
|
+
"run_isolated_corpora",
|
|
74
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, Self
|
|
7
|
+
|
|
8
|
+
from .contracts import (
|
|
9
|
+
CatalogRule,
|
|
10
|
+
DefaultLevel,
|
|
11
|
+
ExpectedOutcome,
|
|
12
|
+
Language,
|
|
13
|
+
RuleOrigin,
|
|
14
|
+
RuleSpec,
|
|
15
|
+
RuleStatus,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class RuleCatalog:
|
|
25
|
+
rules: tuple[CatalogRule, ...]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_rules(cls, rules: Iterable[CatalogRule]) -> Self:
|
|
29
|
+
collected = tuple(rules)
|
|
30
|
+
identifiers = [rule.identifier for rule in collected]
|
|
31
|
+
if len(identifiers) != len(set(identifiers)):
|
|
32
|
+
msg = "catalog rule identifiers must be unique"
|
|
33
|
+
raise ValueError(msg)
|
|
34
|
+
return cls(tuple(sorted(collected, key=lambda rule: rule.identifier)))
|
|
35
|
+
|
|
36
|
+
def get(self, identifier: str) -> CatalogRule | None:
|
|
37
|
+
return next((rule for rule in self.rules if rule.identifier == identifier), None)
|
|
38
|
+
|
|
39
|
+
def filter(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
language: Language | None = None,
|
|
43
|
+
origin: RuleOrigin | None = None,
|
|
44
|
+
configurable: bool | None = None,
|
|
45
|
+
) -> tuple[CatalogRule, ...]:
|
|
46
|
+
return tuple(
|
|
47
|
+
rule
|
|
48
|
+
for rule in self.rules
|
|
49
|
+
if (language is None or language in rule.languages)
|
|
50
|
+
and (origin is None or rule.origin is origin)
|
|
51
|
+
and (configurable is None or rule.configurable is configurable)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class DocumentedRule:
|
|
57
|
+
spec: RuleSpec
|
|
58
|
+
default_level: DefaultLevel
|
|
59
|
+
source: PurePosixPath
|
|
60
|
+
test: PurePosixPath
|
|
61
|
+
status: RuleStatus = RuleStatus.ACTIVE
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if self.status is not RuleStatus.ACTIVE:
|
|
65
|
+
msg = "documented live rules must have active status; lifecycle tombstones are separate"
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
for path in (self.source, self.test):
|
|
68
|
+
if path.is_absolute() or ".." in path.parts or not path.name or "\\" in path.as_posix():
|
|
69
|
+
msg = "catalog source and test paths must be safe repository-relative paths"
|
|
70
|
+
raise ValueError(msg)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class RuleCatalogDocument:
|
|
75
|
+
rules: tuple[DocumentedRule, ...]
|
|
76
|
+
schema_version: int = 1
|
|
77
|
+
|
|
78
|
+
def __post_init__(self) -> None:
|
|
79
|
+
if self.schema_version != 1:
|
|
80
|
+
msg = "unsupported rule catalog schema version"
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
keys = tuple(rule.spec.key for rule in self.rules)
|
|
83
|
+
if len(keys) != len(set(keys)):
|
|
84
|
+
msg = "catalog rule keys must be unique"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
aliases = tuple(f"{rule.spec.engine.value}:{alias}" for rule in self.rules for alias in rule.spec.aliases)
|
|
87
|
+
if len(aliases) != len(set(aliases)) or set(aliases) & set(keys):
|
|
88
|
+
msg = "catalog aliases must be unique and must not shadow live rule keys"
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
|
|
91
|
+
def as_public_dict(self) -> dict[str, object]:
|
|
92
|
+
return {
|
|
93
|
+
"schemaVersion": self.schema_version,
|
|
94
|
+
"rules": [self._rule_dict(rule) for rule in sorted(self.rules, key=lambda item: item.spec.key)],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _rule_dict(rule: DocumentedRule) -> dict[str, object]:
|
|
99
|
+
spec = rule.spec
|
|
100
|
+
examples = sorted(
|
|
101
|
+
(example for example in spec.examples if example.public),
|
|
102
|
+
key=lambda example: example.example_id,
|
|
103
|
+
)
|
|
104
|
+
return {
|
|
105
|
+
"key": spec.key,
|
|
106
|
+
"engine": spec.engine.value,
|
|
107
|
+
"id": spec.rule_id,
|
|
108
|
+
"code": spec.code,
|
|
109
|
+
"summary": spec.summary,
|
|
110
|
+
"rationale": spec.rationale,
|
|
111
|
+
"remediation": spec.remediation,
|
|
112
|
+
"category": spec.category.value,
|
|
113
|
+
"languages": sorted(language.value for language in spec.languages),
|
|
114
|
+
"defaultLevel": rule.default_level.value,
|
|
115
|
+
"autofix": spec.autofix.value,
|
|
116
|
+
"status": rule.status.value,
|
|
117
|
+
"aliases": sorted(spec.aliases),
|
|
118
|
+
"limitations": list(spec.limitations),
|
|
119
|
+
"filePatterns": list(spec.file_patterns),
|
|
120
|
+
"messageIds": list(spec.message_ids),
|
|
121
|
+
"optionsSchema": json.loads(spec.options_schema) if spec.options_schema is not None else None,
|
|
122
|
+
"references": list(spec.references),
|
|
123
|
+
"since": spec.since,
|
|
124
|
+
"source": rule.source.as_posix(),
|
|
125
|
+
"test": rule.test.as_posix(),
|
|
126
|
+
"examples": [
|
|
127
|
+
{
|
|
128
|
+
"id": example.example_id,
|
|
129
|
+
"scenarioId": example.scenario,
|
|
130
|
+
"title": example.title,
|
|
131
|
+
"outcome": "reject" if example.outcome is ExpectedOutcome.MATCH else "accept",
|
|
132
|
+
"focusPath": example.focus_path.as_posix(),
|
|
133
|
+
"expectedCount": example.expected_count,
|
|
134
|
+
"files": [
|
|
135
|
+
{"path": item.path.as_posix(), "source": item.source}
|
|
136
|
+
for item in sorted(example.files, key=lambda item: item.path)
|
|
137
|
+
],
|
|
138
|
+
"fixedFiles": [
|
|
139
|
+
{"path": item.path.as_posix(), "source": item.source}
|
|
140
|
+
for item in sorted(example.fixed_files, key=lambda item: item.path)
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
for example in examples
|
|
144
|
+
],
|
|
145
|
+
}
|