code-constraints 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_constraints/__init__.py +1 -0
- code_constraints/cli/__init__.py +0 -0
- code_constraints/cli/__main__.py +1555 -0
- code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
- code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
- code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
- code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
- code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
- code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
- code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
- code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
- code_constraints/cli/depstamp.py +118 -0
- code_constraints/cli/detect.py +77 -0
- code_constraints/cli/interactive.py +304 -0
- code_constraints/cli/scaffold.py +602 -0
- code_constraints/cli/update.py +157 -0
- code_constraints/core/__init__.py +41 -0
- code_constraints/core/annotations.py +217 -0
- code_constraints/core/associations.py +134 -0
- code_constraints/core/diff.py +302 -0
- code_constraints/core/editor_io.py +280 -0
- code_constraints/core/graph_model.py +681 -0
- code_constraints/core/keys.py +105 -0
- code_constraints/core/model.py +294 -0
- code_constraints/core/model_io.py +65 -0
- code_constraints/core/receivers.py +34 -0
- code_constraints/core/rules.py +177 -0
- code_constraints/core/rulesdoc.py +208 -0
- code_constraints/core/tags.py +114 -0
- code_constraints/core/ts_fingerprint.py +88 -0
- code_constraints/core/xmi_reader.py +358 -0
- code_constraints/core/xmi_writer.py +373 -0
- code_constraints/csharp/__init__.py +3 -0
- code_constraints/csharp/activity.py +250 -0
- code_constraints/csharp/conformance.py +331 -0
- code_constraints/csharp/fingerprint.py +274 -0
- code_constraints/csharp/parser.py +436 -0
- code_constraints/csharp/rules_extract.py +78 -0
- code_constraints/csharp/sequence.py +295 -0
- code_constraints/enforce/__init__.py +15 -0
- code_constraints/enforce/engine.py +122 -0
- code_constraints/enforce/model.py +74 -0
- code_constraints/julia/__init__.py +5 -0
- code_constraints/julia/conformance.py +282 -0
- code_constraints/julia/fingerprint.py +226 -0
- code_constraints/julia/parser.py +523 -0
- code_constraints/julia/rules_extract.py +216 -0
- code_constraints/lint/__init__.py +10 -0
- code_constraints/lint/baseline.py +96 -0
- code_constraints/lint/config.py +239 -0
- code_constraints/lint/engine.py +179 -0
- code_constraints/lint/pipeline.py +108 -0
- code_constraints/lint/report.py +151 -0
- code_constraints/lint/rules/__init__.py +50 -0
- code_constraints/lint/rules/base.py +200 -0
- code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
- code_constraints/lint/rules/dangling_classes.py +98 -0
- code_constraints/lint/rules/forbidden_package_references.py +47 -0
- code_constraints/lint/rules/forbidden_references.py +48 -0
- code_constraints/lint/rules/frozen_members.py +67 -0
- code_constraints/lint/rules/frozen_rules.py +105 -0
- code_constraints/lint/rules/implementation_locks.py +156 -0
- code_constraints/lint/rules/layer_dependencies.py +92 -0
- code_constraints/lint/rules/max_class_fanout.py +41 -0
- code_constraints/lint/rules/no_new_classes.py +27 -0
- code_constraints/lint/rules/no_removed_classes.py +27 -0
- code_constraints/lint/rules/reference_architecture.py +111 -0
- code_constraints/lint/rules/subclass_naming.py +71 -0
- code_constraints/lint/rules/tag_conformance.py +76 -0
- code_constraints/lock/__init__.py +73 -0
- code_constraints/lock/engine.py +395 -0
- code_constraints/lock/model.py +235 -0
- code_constraints/lock/store.py +144 -0
- code_constraints/lua/__init__.py +5 -0
- code_constraints/lua/conformance.py +239 -0
- code_constraints/lua/fingerprint.py +252 -0
- code_constraints/lua/parser.py +500 -0
- code_constraints/lua/rules_extract.py +55 -0
- code_constraints/mcp/__init__.py +20 -0
- code_constraints/mcp/__main__.py +73 -0
- code_constraints/mcp/server.py +1203 -0
- code_constraints/odin/__init__.py +5 -0
- code_constraints/odin/conformance.py +244 -0
- code_constraints/odin/fingerprint.py +159 -0
- code_constraints/odin/parser.py +471 -0
- code_constraints/odin/rules_extract.py +38 -0
- code_constraints/python/__init__.py +3 -0
- code_constraints/python/activity.py +278 -0
- code_constraints/python/conformance.py +249 -0
- code_constraints/python/fingerprint.py +231 -0
- code_constraints/python/parser.py +330 -0
- code_constraints/python/rules_extract.py +83 -0
- code_constraints/python/sequence.py +257 -0
- code_constraints/reference/__init__.py +15 -0
- code_constraints/reference/compare.py +356 -0
- code_constraints/reference/report.py +38 -0
- code_constraints/svelte/__init__.py +3 -0
- code_constraints/svelte/parser.py +523 -0
- code_constraints/typescript/__init__.py +3 -0
- code_constraints/typescript/parser.py +590 -0
- code_constraints/waivers/__init__.py +89 -0
- code_constraints/waivers/collect.py +167 -0
- code_constraints/waivers/model.py +90 -0
- code_constraints/waivers/ops.py +150 -0
- code_constraints/waivers/review.py +156 -0
- code_constraints/waivers/store.py +300 -0
- code_constraints/web/__init__.py +0 -0
- code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
- code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
- code_constraints/web/_static/index.html +13 -0
- code_constraints/web/app.py +1076 -0
- code_constraints-0.1.0.dist-info/METADATA +663 -0
- code_constraints-0.1.0.dist-info/RECORD +116 -0
- code_constraints-0.1.0.dist-info/WHEEL +4 -0
- code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
- code_constraints-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Rule: cap the number of distinct outgoing class references on any class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from code_constraints.core.model import DiffStatus
|
|
8
|
+
from code_constraints.lint.rules import register
|
|
9
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, Violation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register("max-class-fanout")
|
|
13
|
+
class MaxClassFanout(Rule):
|
|
14
|
+
"""Options:
|
|
15
|
+
limit: int, maximum allowed distinct outgoing references (default 10)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
19
|
+
try:
|
|
20
|
+
limit = int(self.options.get("limit", 10))
|
|
21
|
+
except (TypeError, ValueError) as exc:
|
|
22
|
+
raise ValueError(f"max-class-fanout: 'limit' must be an integer: {exc}") from exc
|
|
23
|
+
for cls in ctx.project.iter_classes():
|
|
24
|
+
if cls.status == DiffStatus.REMOVED:
|
|
25
|
+
continue
|
|
26
|
+
if self.is_ignored(cls.qualified_name):
|
|
27
|
+
continue
|
|
28
|
+
if self.scope == "diff" and ctx.has_diff:
|
|
29
|
+
if cls.status not in (DiffStatus.ADDED, DiffStatus.CHANGED):
|
|
30
|
+
continue
|
|
31
|
+
fanout = len(ctx.outgoing_refs.get(cls.qualified_name, set()))
|
|
32
|
+
if fanout <= limit:
|
|
33
|
+
continue
|
|
34
|
+
yield self.emit(
|
|
35
|
+
qualified_name=cls.qualified_name,
|
|
36
|
+
message=self.message_for(
|
|
37
|
+
qualified_name=cls.qualified_name, fanout=fanout, limit=limit
|
|
38
|
+
)
|
|
39
|
+
or f"Class '{cls.qualified_name}' has fanout {fanout} (limit: {limit}).",
|
|
40
|
+
location=cls.location,
|
|
41
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Rule: forbid adding new classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from code_constraints.core.model import DiffStatus
|
|
8
|
+
from code_constraints.lint.rules import register
|
|
9
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, Violation, changed_classes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register("no-new-classes")
|
|
13
|
+
class NoNewClasses(Rule):
|
|
14
|
+
default_scope = "diff"
|
|
15
|
+
|
|
16
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
17
|
+
if not ctx.has_diff:
|
|
18
|
+
return
|
|
19
|
+
for cls in changed_classes(ctx, only_status=DiffStatus.ADDED):
|
|
20
|
+
if self.is_ignored(cls.qualified_name):
|
|
21
|
+
continue
|
|
22
|
+
yield self.emit(
|
|
23
|
+
qualified_name=cls.qualified_name,
|
|
24
|
+
message=self.message_for(qualified_name=cls.qualified_name)
|
|
25
|
+
or f"New class '{cls.qualified_name}' was added.",
|
|
26
|
+
location=cls.location,
|
|
27
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Rule: forbid removing classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
from code_constraints.core.model import DiffStatus
|
|
8
|
+
from code_constraints.lint.rules import register
|
|
9
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, Violation, changed_classes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register("no-removed-classes")
|
|
13
|
+
class NoRemovedClasses(Rule):
|
|
14
|
+
default_scope = "diff"
|
|
15
|
+
|
|
16
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
17
|
+
if not ctx.has_diff:
|
|
18
|
+
return
|
|
19
|
+
for cls in changed_classes(ctx, only_status=DiffStatus.REMOVED):
|
|
20
|
+
if self.is_ignored(cls.qualified_name):
|
|
21
|
+
continue
|
|
22
|
+
yield self.emit(
|
|
23
|
+
qualified_name=cls.qualified_name,
|
|
24
|
+
message=self.message_for(qualified_name=cls.qualified_name)
|
|
25
|
+
or f"Class '{cls.qualified_name}' was removed.",
|
|
26
|
+
location=cls.location,
|
|
27
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Rule: the code must not deviate structurally from the reference model.
|
|
2
|
+
|
|
3
|
+
The adapter for the reference gate (`code_constraints.reference`). Where the
|
|
4
|
+
other rules are scalpels — each one enforcing exactly the law you wrote down —
|
|
5
|
+
this one is a wall: *any* structural difference from the committed
|
|
6
|
+
`.cdec/reference.xmi` fails.
|
|
7
|
+
|
|
8
|
+
It earns its place beside `frozen-members` because the two sit on different
|
|
9
|
+
comparison engines. The diff engine matches members by signature and only calls
|
|
10
|
+
a matched member changed when its *rule tags* differ, so it is blind to
|
|
11
|
+
visibility changes (`public` -> `private`), modifier changes (`static`,
|
|
12
|
+
`abstract`, `readonly`) and class-kind changes. The reference comparator walks
|
|
13
|
+
both models field by field and catches all of them. A pull request that flips a
|
|
14
|
+
public method to private passes `frozen-members` and fails this.
|
|
15
|
+
|
|
16
|
+
Generating the reference is `cdec check --automatic-exceptions reference`, which
|
|
17
|
+
is what `accept_current_state` below does: re-snapshot the source and commit the
|
|
18
|
+
new model in the same pull request, so the architectural delta is reviewable.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Iterable
|
|
25
|
+
|
|
26
|
+
from code_constraints.lint.rules import register
|
|
27
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, RuleSkipped, Violation
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register("reference-architecture")
|
|
31
|
+
class ReferenceArchitecture(Rule):
|
|
32
|
+
"""Options:
|
|
33
|
+
reference: path to the reference model, relative to the project root
|
|
34
|
+
(default: the project's `reference:` setting, else
|
|
35
|
+
`.cdec/reference.xmi`).
|
|
36
|
+
categories: optional list[str] of deviation categories to enforce, e.g.
|
|
37
|
+
["class-removed", "operation-changed"]. Default: all of them.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
supports_auto_accept = True
|
|
41
|
+
|
|
42
|
+
def reference_path(self, ctx: RuleContext) -> Path:
|
|
43
|
+
configured = self.options.get("reference")
|
|
44
|
+
if configured:
|
|
45
|
+
base = ctx.config_dir.parent if ctx.config_dir else Path(".")
|
|
46
|
+
path = Path(str(configured))
|
|
47
|
+
return path if path.is_absolute() else (base / path)
|
|
48
|
+
if ctx.reference_path is not None:
|
|
49
|
+
return ctx.reference_path
|
|
50
|
+
raise RuleSkipped("no reference model configured")
|
|
51
|
+
|
|
52
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
53
|
+
path = self.reference_path(ctx)
|
|
54
|
+
if not path.is_file():
|
|
55
|
+
raise RuleSkipped(
|
|
56
|
+
f"reference model not found: {path}. Snapshot one with "
|
|
57
|
+
f"`cdec check --automatic-exceptions reference`."
|
|
58
|
+
)
|
|
59
|
+
from code_constraints.core.model_io import load_model
|
|
60
|
+
from code_constraints.reference import compare_to_reference
|
|
61
|
+
|
|
62
|
+
if ctx.source is None or not ctx.language:
|
|
63
|
+
raise RuleSkipped("no source tree resolved")
|
|
64
|
+
|
|
65
|
+
# The rule compares against the *current* parse, never the annotated
|
|
66
|
+
# diff project: a removed class is still present (marked REMOVED) in the
|
|
67
|
+
# annotated model, and the gate must see it as gone.
|
|
68
|
+
from code_constraints.lint.pipeline import parse_source
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
deviations = compare_to_reference(
|
|
72
|
+
load_model(path), parse_source(ctx.source, ctx.language)
|
|
73
|
+
)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise RuleSkipped(str(exc)) from exc
|
|
76
|
+
|
|
77
|
+
only = {str(c) for c in (self.options.get("categories") or [])}
|
|
78
|
+
for deviation in deviations:
|
|
79
|
+
if only and deviation.category not in only:
|
|
80
|
+
continue
|
|
81
|
+
if self.is_ignored(deviation.qualified_name):
|
|
82
|
+
continue
|
|
83
|
+
yield Violation(
|
|
84
|
+
rule_id=self.rule_id,
|
|
85
|
+
severity=self.severity,
|
|
86
|
+
qualified_name=deviation.qualified_name,
|
|
87
|
+
message=self.message_for(
|
|
88
|
+
qualified_name=deviation.qualified_name,
|
|
89
|
+
category=deviation.category,
|
|
90
|
+
member=deviation.member or "",
|
|
91
|
+
message=deviation.message,
|
|
92
|
+
)
|
|
93
|
+
or deviation.message,
|
|
94
|
+
signature=deviation.member or deviation.category,
|
|
95
|
+
key_engine="reference",
|
|
96
|
+
key_rule=deviation.category,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def accept_current_state(self, ctx: RuleContext, *, force: bool = False) -> list[str]:
|
|
100
|
+
"""Re-snapshot the reference from the current source."""
|
|
101
|
+
if ctx.source is None or not ctx.language:
|
|
102
|
+
raise RuleSkipped("no source tree resolved")
|
|
103
|
+
from code_constraints.core.model_io import save_model
|
|
104
|
+
from code_constraints.lint.pipeline import parse_source
|
|
105
|
+
|
|
106
|
+
path = self.reference_path(ctx)
|
|
107
|
+
project = parse_source(ctx.source, ctx.language)
|
|
108
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
save_model(project, path)
|
|
110
|
+
n_classes = sum(1 for _ in project.iter_classes())
|
|
111
|
+
return [f" wrote {path} ({n_classes} class(es) snapshotted from {ctx.source})"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Rule: enforce a naming pattern on classes inheriting from a base/interface.
|
|
2
|
+
|
|
3
|
+
Example: every implementation of `IFactory` must end in `Factory`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
from code_constraints.core.model import DiffStatus
|
|
12
|
+
from code_constraints.lint.rules import register
|
|
13
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, Violation, match_any_glob
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register("subclass-naming")
|
|
17
|
+
class SubclassNaming(Rule):
|
|
18
|
+
"""Options:
|
|
19
|
+
base: glob pattern matched against entries in `Class.bases`
|
|
20
|
+
(e.g. "IFactory", "*.IFactory")
|
|
21
|
+
name_pattern: regex applied to the class name (full match)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
25
|
+
base_pattern = self.options.get("base")
|
|
26
|
+
name_pattern = self.options.get("name_pattern")
|
|
27
|
+
if not base_pattern or not name_pattern:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
regex = re.compile(name_pattern)
|
|
31
|
+
except re.error as exc: # pragma: no cover - config-validation territory
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"subclass-naming: invalid name_pattern regex {name_pattern!r}: {exc}"
|
|
34
|
+
) from exc
|
|
35
|
+
|
|
36
|
+
base_globs = base_pattern if isinstance(base_pattern, list) else [base_pattern]
|
|
37
|
+
|
|
38
|
+
for cls in ctx.project.iter_classes():
|
|
39
|
+
if cls.status == DiffStatus.REMOVED:
|
|
40
|
+
continue
|
|
41
|
+
if self.is_ignored(cls.qualified_name):
|
|
42
|
+
continue
|
|
43
|
+
if self.scope == "diff" and ctx.has_diff:
|
|
44
|
+
if cls.status not in (DiffStatus.ADDED, DiffStatus.CHANGED):
|
|
45
|
+
continue
|
|
46
|
+
# Match if any of the class's bases matches the configured base.
|
|
47
|
+
# We match both the literal base text and its short name (last
|
|
48
|
+
# dotted segment) so callers can write either "IFactory" or
|
|
49
|
+
# "myapp.factories.IFactory".
|
|
50
|
+
matched = False
|
|
51
|
+
for base in cls.bases:
|
|
52
|
+
short = base.split(".")[-1]
|
|
53
|
+
if match_any_glob(base, base_globs) or match_any_glob(short, base_globs):
|
|
54
|
+
matched = True
|
|
55
|
+
break
|
|
56
|
+
if not matched:
|
|
57
|
+
continue
|
|
58
|
+
if regex.fullmatch(cls.name):
|
|
59
|
+
continue
|
|
60
|
+
yield self.emit(
|
|
61
|
+
qualified_name=cls.qualified_name,
|
|
62
|
+
message=self.message_for(
|
|
63
|
+
qualified_name=cls.qualified_name,
|
|
64
|
+
name=cls.name,
|
|
65
|
+
base=base_pattern,
|
|
66
|
+
pattern=name_pattern,
|
|
67
|
+
)
|
|
68
|
+
or f"Class '{cls.qualified_name}' inherits from {base_pattern!r} "
|
|
69
|
+
f"but its name does not match /{name_pattern}/.",
|
|
70
|
+
location=cls.location,
|
|
71
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Rule: the implementation must honour the constraint tags written on it.
|
|
2
|
+
|
|
3
|
+
This is the adapter for the conformance engine (`code_constraints.enforce`,
|
|
4
|
+
"Engine B"). Every other rule in this package reads only the model; this one
|
|
5
|
+
re-parses the source and inspects method bodies, which is why it needs
|
|
6
|
+
`ctx.source` and `ctx.language`.
|
|
7
|
+
|
|
8
|
+
The engine stays an independent package with its own result type — the rule
|
|
9
|
+
imports it lazily and translates `Finding`s into `Violation`s. What the
|
|
10
|
+
`rules.yaml` entry buys is uniformity: tag conformance is now configured,
|
|
11
|
+
reported, ignored and excepted exactly like every other law in the file, instead
|
|
12
|
+
of through a second command with a second report and a second exit code.
|
|
13
|
+
|
|
14
|
+
Violations key under the `enforce` engine (`F-…`), not under the rules entry, so
|
|
15
|
+
renaming the entry never invalidates a granted exception.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Iterable
|
|
21
|
+
|
|
22
|
+
from code_constraints.core.model import SourceLocation
|
|
23
|
+
from code_constraints.lint.rules import register
|
|
24
|
+
from code_constraints.lint.rules.base import Rule, RuleContext, RuleSkipped, Violation
|
|
25
|
+
|
|
26
|
+
#: Languages whose parsers recognise constraint tags. The other supported
|
|
27
|
+
#: languages parse and diff fine but have no tag syntax, so a `tag-conformance`
|
|
28
|
+
#: rule there would report "clean" while checking nothing.
|
|
29
|
+
TAGGED_LANGUAGES = ("python", "csharp", "odin", "lua", "julia")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@register("tag-conformance")
|
|
33
|
+
class TagConformance(Rule):
|
|
34
|
+
"""Options:
|
|
35
|
+
rules: optional list[str] of catalog ids to check (default: all of them),
|
|
36
|
+
e.g. ["no-instantiation", "factory"] to adopt one tag at a time.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def check(self, ctx: RuleContext) -> Iterable[Violation]:
|
|
40
|
+
if ctx.source is None or not ctx.language:
|
|
41
|
+
raise RuleSkipped("no source tree resolved")
|
|
42
|
+
if ctx.language not in TAGGED_LANGUAGES:
|
|
43
|
+
raise RuleSkipped(
|
|
44
|
+
f"{ctx.language} has no constraint-tag syntax; supported: "
|
|
45
|
+
f"{', '.join(TAGGED_LANGUAGES)}"
|
|
46
|
+
)
|
|
47
|
+
from code_constraints.enforce import enforce as run_enforce
|
|
48
|
+
|
|
49
|
+
only = {str(r) for r in (self.options.get("rules") or [])}
|
|
50
|
+
findings = run_enforce(ctx.source, ctx.language)
|
|
51
|
+
for finding in findings:
|
|
52
|
+
if only and finding.rule not in only:
|
|
53
|
+
continue
|
|
54
|
+
if self.is_ignored(finding.qualified_name):
|
|
55
|
+
continue
|
|
56
|
+
location = (
|
|
57
|
+
SourceLocation(file=finding.file, start_line=finding.line, end_line=finding.line)
|
|
58
|
+
if finding.file
|
|
59
|
+
else None
|
|
60
|
+
)
|
|
61
|
+
yield Violation(
|
|
62
|
+
rule_id=self.rule_id,
|
|
63
|
+
severity=self.severity,
|
|
64
|
+
qualified_name=finding.qualified_name,
|
|
65
|
+
message=self.message_for(
|
|
66
|
+
qualified_name=finding.qualified_name,
|
|
67
|
+
rule=finding.rule,
|
|
68
|
+
detail=finding.detail,
|
|
69
|
+
message=finding.message,
|
|
70
|
+
)
|
|
71
|
+
or finding.message,
|
|
72
|
+
location=location,
|
|
73
|
+
signature=finding.detail,
|
|
74
|
+
key_engine="enforce",
|
|
75
|
+
key_rule=finding.rule,
|
|
76
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""The implementation-freeze engine (Engine C).
|
|
2
|
+
|
|
3
|
+
Freezes the *body* of a class or function so it cannot change without an
|
|
4
|
+
explicit, reviewable re-baseline. Identity is AST-derived, not line-based, so
|
|
5
|
+
code added above a locked element never trips it.
|
|
6
|
+
|
|
7
|
+
Driven by the `implementation-locks` rule type in `.cdec/rules.yaml`, which is
|
|
8
|
+
what `cdec check` runs. This package stays independent of the other engines:
|
|
9
|
+
the rule is a thin adapter over the entry points below.
|
|
10
|
+
|
|
11
|
+
Public surface:
|
|
12
|
+
|
|
13
|
+
from code_constraints.lock import (
|
|
14
|
+
LockOptions, check_locks, update_locks, collect_targets,
|
|
15
|
+
load_locks, write_locks, format_report,
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from code_constraints.lock.engine import (
|
|
20
|
+
BYPASS_ENV,
|
|
21
|
+
BYPASS_REASON_ENV,
|
|
22
|
+
LockOptions,
|
|
23
|
+
UnsupportedLockLanguage,
|
|
24
|
+
UpdateResult,
|
|
25
|
+
check_locks,
|
|
26
|
+
collect_targets,
|
|
27
|
+
is_locked_target,
|
|
28
|
+
resolve_entries_for_removal,
|
|
29
|
+
update_locks,
|
|
30
|
+
)
|
|
31
|
+
from code_constraints.lock.model import (
|
|
32
|
+
LOCK_RULE,
|
|
33
|
+
LockEntry,
|
|
34
|
+
LockReport,
|
|
35
|
+
LockTarget,
|
|
36
|
+
LockViolation,
|
|
37
|
+
format_report,
|
|
38
|
+
match_any_glob,
|
|
39
|
+
report_to_json,
|
|
40
|
+
)
|
|
41
|
+
from code_constraints.lock.store import (
|
|
42
|
+
LOCKFILE_VERSION,
|
|
43
|
+
LOCKS_FILENAME,
|
|
44
|
+
LockfileError,
|
|
45
|
+
load_locks,
|
|
46
|
+
write_locks,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"BYPASS_ENV",
|
|
51
|
+
"BYPASS_REASON_ENV",
|
|
52
|
+
"LOCKFILE_VERSION",
|
|
53
|
+
"LOCKS_FILENAME",
|
|
54
|
+
"LOCK_RULE",
|
|
55
|
+
"LockEntry",
|
|
56
|
+
"LockOptions",
|
|
57
|
+
"LockReport",
|
|
58
|
+
"LockTarget",
|
|
59
|
+
"LockViolation",
|
|
60
|
+
"LockfileError",
|
|
61
|
+
"UnsupportedLockLanguage",
|
|
62
|
+
"UpdateResult",
|
|
63
|
+
"check_locks",
|
|
64
|
+
"collect_targets",
|
|
65
|
+
"format_report",
|
|
66
|
+
"is_locked_target",
|
|
67
|
+
"load_locks",
|
|
68
|
+
"match_any_glob",
|
|
69
|
+
"report_to_json",
|
|
70
|
+
"resolve_entries_for_removal",
|
|
71
|
+
"update_locks",
|
|
72
|
+
"write_locks",
|
|
73
|
+
]
|