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,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from sarj_standards.libs.adoption import transaction
|
|
10
|
+
from sarj_standards.libs.rules import RuleEngine, RuleSelector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_CODE = re.compile(r"SARJ(?P<number>[0-9]{3})")
|
|
18
|
+
_BANDS = MappingProxyType(
|
|
19
|
+
{RuleEngine.PYTHON: range(400, 500), RuleEngine.SQL: range(100, 200), RuleEngine.IAC: range(200, 300)}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class AuthoringPlan:
|
|
25
|
+
selector: RuleSelector
|
|
26
|
+
code: str | None
|
|
27
|
+
files: tuple[tuple[Path, str], ...]
|
|
28
|
+
|
|
29
|
+
def render(self, root: Path) -> str:
|
|
30
|
+
lines = [f"rule: {self.selector}", f"code: {self.code or 'engine-native'}"]
|
|
31
|
+
lines.extend(f"create: {path.relative_to(root)}" for path, _ in self.files)
|
|
32
|
+
registry = _registry_path(root, self.selector.engine).relative_to(root)
|
|
33
|
+
lines.extend(
|
|
34
|
+
(
|
|
35
|
+
f"next: implement the TODOs and register the rule in {registry}",
|
|
36
|
+
f"then: run the focused test and `code-standards maintain rules verify {self.selector}`",
|
|
37
|
+
f"then: run `code-standards maintain rules evaluate --rule {self.selector} --scope corpus`",
|
|
38
|
+
f"finally: run `code-standards maintain rules prepare {self.selector}`",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class VerificationResult:
|
|
46
|
+
status: int
|
|
47
|
+
message: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def verify(root: Path, selector: RuleSelector) -> VerificationResult:
|
|
51
|
+
from sarj_standards.libs.repository import ( # ruff: ignore[import-outside-top-level] -- expensive engines load on demand
|
|
52
|
+
rule_catalog_artifact,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
repository = root.resolve()
|
|
56
|
+
catalog = rule_catalog_artifact.build(repository)
|
|
57
|
+
documented = next((item for item in catalog.rules if item.spec.key == str(selector)), None)
|
|
58
|
+
if documented is None:
|
|
59
|
+
msg = f"unknown live rule selector: {selector}; register the implementation, then rerun verify"
|
|
60
|
+
return VerificationResult(2, msg)
|
|
61
|
+
examples = tuple(example for example in documented.spec.examples if example.public)
|
|
62
|
+
outcomes = {example.outcome.value for example in examples}
|
|
63
|
+
if outcomes != {"match", "no-match"}:
|
|
64
|
+
return VerificationResult(1, f"incomplete: {selector} needs public match and no-match examples")
|
|
65
|
+
paths = (repository / documented.source, repository / documented.test)
|
|
66
|
+
for path in paths:
|
|
67
|
+
if not path.is_file():
|
|
68
|
+
return VerificationResult(1, f"incomplete: expected authored file {path.relative_to(repository)}")
|
|
69
|
+
if "TODO" in path.read_text(encoding="utf-8"):
|
|
70
|
+
return VerificationResult(1, f"incomplete: resolve TODOs in {path.relative_to(repository)}")
|
|
71
|
+
return VerificationResult(
|
|
72
|
+
0,
|
|
73
|
+
f"ok: {selector} is registered with complete metadata and public accept/reject examples",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def plan_new(root: Path, selector: RuleSelector, *, category: str, summary: str) -> AuthoringPlan:
|
|
78
|
+
if selector.engine is RuleEngine.TEXT:
|
|
79
|
+
msg = "text rules use the shared textlint catalog and are not scaffolded yet"
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
code = _next_code(root, selector.engine)
|
|
82
|
+
slug = str(selector.rule_id)
|
|
83
|
+
snake = slug.replace("-", "_")
|
|
84
|
+
class_name = "".join(part.capitalize() for part in slug.split("-"))
|
|
85
|
+
if selector.engine is RuleEngine.ESLINT:
|
|
86
|
+
implementation = root / "packages/typescript/src/rules" / f"{slug}.ts"
|
|
87
|
+
test = root / "packages/typescript/tests/rules" / f"{slug}.test.ts"
|
|
88
|
+
files = (
|
|
89
|
+
(implementation, _eslint_implementation(slug, class_name, category, summary)),
|
|
90
|
+
(test, _eslint_test(slug, class_name)),
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
package = selector.engine.value
|
|
94
|
+
module = f"sarj_{package}_lint"
|
|
95
|
+
implementation = root / f"packages/{package}/src/{module}/rules" / f"{snake}.py"
|
|
96
|
+
test = root / f"packages/{package}/tests/rules" / f"test_{snake}.py"
|
|
97
|
+
files = (
|
|
98
|
+
(
|
|
99
|
+
implementation,
|
|
100
|
+
_python_implementation(module, class_name, slug, code or "", category=category, summary=summary),
|
|
101
|
+
),
|
|
102
|
+
(test, _python_test(module, class_name, snake)),
|
|
103
|
+
)
|
|
104
|
+
conflicts = [path for path, _ in files if path.exists()]
|
|
105
|
+
if conflicts:
|
|
106
|
+
msg = "rule scaffold target already exists: " + ", ".join(str(path.relative_to(root)) for path in conflicts)
|
|
107
|
+
raise FileExistsError(msg)
|
|
108
|
+
return AuthoringPlan(selector, code, files)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def apply(plan: AuthoringPlan, root: Path) -> None:
|
|
112
|
+
mutation = transaction.FileTransaction.capture(root, tuple(path for path, _ in plan.files))
|
|
113
|
+
try:
|
|
114
|
+
for path, contents in plan.files:
|
|
115
|
+
transaction.assert_expected(root, path, None)
|
|
116
|
+
transaction.atomic_write_text(root, path, contents)
|
|
117
|
+
mutation.mark_written(path)
|
|
118
|
+
except BaseException:
|
|
119
|
+
_ = mutation.rollback()
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _registry_path(root: Path, engine: RuleEngine) -> Path:
|
|
124
|
+
if engine is RuleEngine.ESLINT:
|
|
125
|
+
return root / "packages/typescript/src/index.ts"
|
|
126
|
+
package = engine.value
|
|
127
|
+
return root / f"packages/{package}/src/sarj_{package}_lint/rules/_registry.py"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _next_code(root: Path, engine: RuleEngine) -> str | None:
|
|
131
|
+
band = _BANDS.get(engine)
|
|
132
|
+
if band is None:
|
|
133
|
+
return None
|
|
134
|
+
used = {number for number in _code_numbers(_source_texts(root)) if number in band}
|
|
135
|
+
available = max(used, default=band.start - 1) + 1
|
|
136
|
+
if available not in band:
|
|
137
|
+
msg = f"no unreserved SARJ codes remain in the {engine.value} band"
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
return f"SARJ{available:03d}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _source_texts(root: Path) -> Iterable[str]:
|
|
143
|
+
for base in (root / "packages", root / "tests"):
|
|
144
|
+
if base.is_dir():
|
|
145
|
+
for path in base.rglob("*"):
|
|
146
|
+
if path.is_file() and path.suffix in {".py", ".ts", ".json"}:
|
|
147
|
+
try:
|
|
148
|
+
yield path.read_text(encoding="utf-8")
|
|
149
|
+
except UnicodeDecodeError:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _code_numbers(texts: Iterable[str]) -> Iterable[int]:
|
|
154
|
+
for text in texts:
|
|
155
|
+
for match in _CODE.finditer(text):
|
|
156
|
+
yield int(match.group("number"))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _python_implementation(module: str, class_name: str, slug: str, code: str, *, category: str, summary: str) -> str:
|
|
160
|
+
example_factory = module.removeprefix("sarj_").removesuffix("_lint")
|
|
161
|
+
return f'''"""{code} — {summary}"""
|
|
162
|
+
|
|
163
|
+
from __future__ import annotations
|
|
164
|
+
|
|
165
|
+
from pathlib import PurePosixPath
|
|
166
|
+
from typing import TYPE_CHECKING, final, override
|
|
167
|
+
|
|
168
|
+
from {module}.rule_base import AutofixPolicy, Diagnostic, ExampleFile, ExampleOutcome, Rule, RuleCategory, RuleDocumentation, RuleExample
|
|
169
|
+
|
|
170
|
+
if TYPE_CHECKING:
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@final
|
|
175
|
+
class {class_name}(Rule):
|
|
176
|
+
id = "{slug}"
|
|
177
|
+
code = "{code}"
|
|
178
|
+
documentation = RuleDocumentation(
|
|
179
|
+
summary={summary!r},
|
|
180
|
+
rationale="TODO: explain the concrete failure mode.",
|
|
181
|
+
remediation="TODO: give the smallest safe remediation.",
|
|
182
|
+
category=RuleCategory.{category.upper()},
|
|
183
|
+
autofix=AutofixPolicy.NONE,
|
|
184
|
+
limitations=("TODO: document the applicability boundary.",),
|
|
185
|
+
examples=(
|
|
186
|
+
RuleExample(example_id="rejects-antipattern", title="Reject the anti-pattern", outcome=ExampleOutcome.MATCH, files=(ExampleFile.{example_factory}("case.{example_factory}", "TODO invalid example\\n"),), focus_path=PurePosixPath("case.{example_factory}"), expected_count=1, public=True),
|
|
187
|
+
RuleExample(example_id="accepts-alternative", title="Accept the preferred alternative", outcome=ExampleOutcome.NO_MATCH, files=(ExampleFile.{example_factory}("case.{example_factory}", "TODO valid example\\n"),), focus_path=PurePosixPath("case.{example_factory}"), expected_count=0, public=True),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
description = documentation.summary
|
|
191
|
+
|
|
192
|
+
@override
|
|
193
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
194
|
+
raise NotImplementedError("TODO: implement conservative detection")
|
|
195
|
+
'''
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _python_test(module: str, class_name: str, snake: str) -> str:
|
|
199
|
+
return f"""from {module}.rules.{snake} import {class_name}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_documented_examples_are_present() -> None:
|
|
203
|
+
outcomes = {{example.outcome.value for example in {class_name}.documentation.examples}}
|
|
204
|
+
assert outcomes == {{"match", "no-match"}}
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _eslint_implementation(slug: str, name: str, category: str, summary: str) -> str:
|
|
209
|
+
message = name[0].lower() + name[1:]
|
|
210
|
+
return f"""import {{ createRule, type RuleDocumentation }} from "./_docs.js";
|
|
211
|
+
|
|
212
|
+
type MessageIds = "{message}";
|
|
213
|
+
type Options = readonly [];
|
|
214
|
+
|
|
215
|
+
export const {message}Documentation = {{
|
|
216
|
+
summary: {summary!r},
|
|
217
|
+
rationale: "TODO: explain the concrete failure mode.",
|
|
218
|
+
remediation: "TODO: give the smallest safe remediation.",
|
|
219
|
+
category: "{category}",
|
|
220
|
+
limitations: ["TODO: document the applicability boundary."],
|
|
221
|
+
examples: [
|
|
222
|
+
{{ id: "accepts-alternative", title: "Accept the preferred alternative", outcome: "no-match", files: [{{ path: "case.ts", source: "// TODO valid example" }}], focusPath: "case.ts", expectedCount: 0, public: true }},
|
|
223
|
+
{{ id: "rejects-antipattern", title: "Reject the anti-pattern", outcome: "match", files: [{{ path: "case.ts", source: "// TODO invalid example" }}], focusPath: "case.ts", expectedCount: 1, public: true }},
|
|
224
|
+
],
|
|
225
|
+
}} as const satisfies RuleDocumentation;
|
|
226
|
+
|
|
227
|
+
export default createRule<Options, MessageIds>({{
|
|
228
|
+
name: "{slug}",
|
|
229
|
+
meta: {{ type: "problem", docs: {{ description: {message}Documentation.summary }}, schema: [], messages: {{ {message}: {summary!r} }} }},
|
|
230
|
+
defaultOptions: [],
|
|
231
|
+
create() {{ throw new Error("TODO: implement conservative detection"); }},
|
|
232
|
+
}});
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _eslint_test(slug: str, name: str) -> str:
|
|
237
|
+
message = name[0].lower() + name[1:]
|
|
238
|
+
return f"""import {{ describe, expect, it }} from "vitest";
|
|
239
|
+
import {{ {message}Documentation }} from "../../src/rules/{slug}.js";
|
|
240
|
+
|
|
241
|
+
describe("{slug}", () => {{
|
|
242
|
+
it("declares public accept and reject examples", () => {{
|
|
243
|
+
expect({message}Documentation.examples.map((item) => item.outcome).sort()).toEqual(["match", "no-match"]);
|
|
244
|
+
}});
|
|
245
|
+
}});
|
|
246
|
+
"""
|